rippled
Loading...
Searching...
No Matches
ReducedOffer_test.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2022 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <test/jtx.h>
21#include <xrpl/protocol/Feature.h>
22#include <xrpl/protocol/Quality.h>
23#include <xrpl/protocol/jss.h>
24
25#include <initializer_list>
26
27namespace ripple {
28namespace test {
29
31{
32 static auto
34 jtx::Env& env,
35 jtx::Account const& acct,
36 std::uint32_t offer_seq)
37 {
38 Json::Value jvParams;
39 jvParams[jss::offer][jss::account] = acct.human();
40 jvParams[jss::offer][jss::seq] = offer_seq;
41 return env.rpc(
42 "json", "ledger_entry", to_string(jvParams))[jss::result];
43 }
44
45 static bool
47 jtx::Env& env,
48 jtx::Account const& acct,
49 std::uint32_t offerSeq)
50 {
51 Json::Value ledgerOffer = ledgerEntryOffer(env, acct, offerSeq);
52 return !(
53 ledgerOffer.isMember(jss::error) &&
54 ledgerOffer[jss::error].asString() == "entryNotFound");
55 }
56
57 // Common code to clean up unneeded offers.
58 static void
60 jtx::Env& env,
62 list)
63 {
64 for (auto [acct, offerSeq] : list)
65 env(offer_cancel(acct, offerSeq));
66 env.close();
67 }
68
69public:
70 void
72 {
73 testcase("exercise partial cross new XRP/IOU offer Q change");
74
75 using namespace jtx;
76
77 auto const gw = Account{"gateway"};
78 auto const alice = Account{"alice"};
79 auto const bob = Account{"bob"};
80 auto const USD = gw["USD"];
81
82 // Make one test run without fixReducedOffersV1 and one with.
83 for (FeatureBitset features :
84 {supported_amendments() - fixReducedOffersV1,
85 supported_amendments() | fixReducedOffersV1})
86 {
87 Env env{*this, features};
88
89 // Make sure none of the offers we generate are under funded.
90 env.fund(XRP(10'000'000), gw, alice, bob);
91 env.close();
92
93 env(trust(alice, USD(10'000'000)));
94 env(trust(bob, USD(10'000'000)));
95 env.close();
96
97 env(pay(gw, bob, USD(10'000'000)));
98 env.close();
99
100 // Lambda that:
101 // 1. Exercises one offer pair,
102 // 2. Collects the results, and
103 // 3. Cleans up for the next offer pair.
104 // Returns 1 if the crossed offer has a bad rate for the book.
105 auto exerciseOfferPair =
106 [this, &env, &alice, &bob](
107 Amounts const& inLedger,
108 Amounts const& newOffer) -> unsigned int {
109 // Put inLedger offer in the ledger so newOffer can cross it.
110 std::uint32_t const aliceOfferSeq = env.seq(alice);
111 env(offer(alice, inLedger.in, inLedger.out));
112 env.close();
113
114 // Now alice's offer will partially cross bob's offer.
115 STAmount const initialRate = Quality(newOffer).rate();
116 std::uint32_t const bobOfferSeq = env.seq(bob);
117 STAmount const bobInitialBalance = env.balance(bob);
118 STAmount const bobsFee = drops(10);
119 env(offer(bob, newOffer.in, newOffer.out, tfSell),
120 fee(bobsFee));
121 env.close();
122 STAmount const bobFinalBalance = env.balance(bob);
123
124 // alice's offer should be fully crossed and so gone from
125 // the ledger.
126 if (!BEAST_EXPECT(!offerInLedger(env, alice, aliceOfferSeq)))
127 // If the in-ledger offer was not consumed then further
128 // results are meaningless.
129 return 1;
130
131 // bob's offer should be in the ledger, but reduced in size.
132 unsigned int badRate = 1;
133 {
134 Json::Value bobOffer =
135 ledgerEntryOffer(env, bob, bobOfferSeq);
136
137 STAmount const reducedTakerGets = amountFromJson(
138 sfTakerGets, bobOffer[jss::node][sfTakerGets.jsonName]);
139 STAmount const reducedTakerPays = amountFromJson(
140 sfTakerPays, bobOffer[jss::node][sfTakerPays.jsonName]);
141 STAmount const bobGot =
142 env.balance(bob) + bobsFee - bobInitialBalance;
143 BEAST_EXPECT(reducedTakerPays < newOffer.in);
144 BEAST_EXPECT(reducedTakerGets < newOffer.out);
145 STAmount const inLedgerRate =
146 Quality(Amounts{reducedTakerPays, reducedTakerGets})
147 .rate();
148
149 badRate = inLedgerRate > initialRate ? 1 : 0;
150
151 // If the inLedgerRate is less than initial rate, then
152 // incrementing the mantissa of the reduced taker pays
153 // should result in a rate higher than initial. Check
154 // this to verify that the largest allowable TakerPays
155 // was computed.
156 if (badRate == 0)
157 {
158 STAmount const tweakedTakerPays =
159 reducedTakerPays + drops(1);
160 STAmount const tweakedRate =
161 Quality(Amounts{tweakedTakerPays, reducedTakerGets})
162 .rate();
163 BEAST_EXPECT(tweakedRate > initialRate);
164 }
165#if 0
166 std::cout << "Placed rate: " << initialRate
167 << "; in-ledger rate: " << inLedgerRate
168 << "; TakerPays: " << reducedTakerPays
169 << "; TakerGets: " << reducedTakerGets
170 << "; bob already got: " << bobGot << std::endl;
171// #else
172 std::string_view filler =
173 inLedgerRate > initialRate ? "**" : " ";
174 std::cout << "| `" << reducedTakerGets << "` | `"
175 << reducedTakerPays << "` | `" << initialRate
176 << "` | " << filler << "`" << inLedgerRate << "`"
177 << filler << " |`" << std::endl;
178#endif
179 }
180
181 // In preparation for the next iteration make sure the two
182 // offers are gone from the ledger.
184 env, {{alice, aliceOfferSeq}, {bob, bobOfferSeq}});
185 return badRate;
186 };
187
188 // bob's offer (the new offer) is the same every time:
189 Amounts const bobsOffer{
190 STAmount(XRP(1)), STAmount(USD.issue(), 1, 0)};
191
192 // alice's offer has a slightly smaller TakerPays with each
193 // iteration. This should mean that the size of the offer bob
194 // places in the ledger should increase with each iteration.
195 unsigned int blockedCount = 0;
196 for (std::uint64_t mantissaReduce = 1'000'000'000ull;
197 mantissaReduce <= 5'000'000'000ull;
198 mantissaReduce += 20'000'000ull)
199 {
200 STAmount aliceUSD{
201 bobsOffer.out.issue(),
202 bobsOffer.out.mantissa() - mantissaReduce,
203 bobsOffer.out.exponent()};
204 STAmount aliceXRP{
205 bobsOffer.in.issue(), bobsOffer.in.mantissa() - 1};
206 Amounts alicesOffer{aliceUSD, aliceXRP};
207 blockedCount += exerciseOfferPair(alicesOffer, bobsOffer);
208 }
209
210 // If fixReducedOffersV1 is enabled, then none of the test cases
211 // should produce a potentially blocking rate.
212 //
213 // Also verify that if fixReducedOffersV1 is not enabled then
214 // some of the test cases produced a potentially blocking rate.
215 if (features[fixReducedOffersV1])
216 {
217 BEAST_EXPECT(blockedCount == 0);
218 }
219 else
220 {
221 BEAST_EXPECT(blockedCount >= 170);
222 }
223 }
224 }
225
226 void
228 {
229 testcase("exercise partial cross old XRP/IOU offer Q change");
230
231 using namespace jtx;
232
233 auto const gw = Account{"gateway"};
234 auto const alice = Account{"alice"};
235 auto const bob = Account{"bob"};
236 auto const USD = gw["USD"];
237
238 // Make one test run without fixReducedOffersV1 and one with.
239 for (FeatureBitset features :
240 {supported_amendments() - fixReducedOffersV1,
241 supported_amendments() | fixReducedOffersV1})
242 {
243 // Make sure none of the offers we generate are under funded.
244 Env env{*this, features};
245 env.fund(XRP(10'000'000), gw, alice, bob);
246 env.close();
247
248 env(trust(alice, USD(10'000'000)));
249 env(trust(bob, USD(10'000'000)));
250 env.close();
251
252 env(pay(gw, alice, USD(10'000'000)));
253 env.close();
254
255 // Lambda that:
256 // 1. Exercises one offer pair,
257 // 2. Collects the results, and
258 // 3. Cleans up for the next offer pair.
259 auto exerciseOfferPair =
260 [this, &env, &alice, &bob](
261 Amounts const& inLedger,
262 Amounts const& newOffer) -> unsigned int {
263 // Get the inLedger offer into the ledger so newOffer can cross
264 // it.
265 STAmount const initialRate = Quality(inLedger).rate();
266 std::uint32_t const aliceOfferSeq = env.seq(alice);
267 env(offer(alice, inLedger.in, inLedger.out));
268 env.close();
269
270 // Now bob's offer will partially cross alice's offer.
271 std::uint32_t const bobOfferSeq = env.seq(bob);
272 STAmount const aliceInitialBalance = env.balance(alice);
273 env(offer(bob, newOffer.in, newOffer.out));
274 env.close();
275 STAmount const aliceFinalBalance = env.balance(alice);
276
277 // bob's offer should not have made it into the ledger.
278 if (!BEAST_EXPECT(!offerInLedger(env, bob, bobOfferSeq)))
279 {
280 // If the in-ledger offer was not consumed then further
281 // results are meaningless.
283 env, {{alice, aliceOfferSeq}, {bob, bobOfferSeq}});
284 return 1;
285 }
286 // alice's offer should still be in the ledger, but reduced in
287 // size.
288 unsigned int badRate = 1;
289 {
290 Json::Value aliceOffer =
291 ledgerEntryOffer(env, alice, aliceOfferSeq);
292
293 STAmount const reducedTakerGets = amountFromJson(
294 sfTakerGets,
295 aliceOffer[jss::node][sfTakerGets.jsonName]);
296 STAmount const reducedTakerPays = amountFromJson(
297 sfTakerPays,
298 aliceOffer[jss::node][sfTakerPays.jsonName]);
299 STAmount const aliceGot =
300 env.balance(alice) - aliceInitialBalance;
301 BEAST_EXPECT(reducedTakerPays < inLedger.in);
302 BEAST_EXPECT(reducedTakerGets < inLedger.out);
303 STAmount const inLedgerRate =
304 Quality(Amounts{reducedTakerPays, reducedTakerGets})
305 .rate();
306 badRate = inLedgerRate > initialRate ? 1 : 0;
307
308 // If the inLedgerRate is less than initial rate, then
309 // incrementing the mantissa of the reduced taker pays
310 // should result in a rate higher than initial. Check
311 // this to verify that the largest allowable TakerPays
312 // was computed.
313 if (badRate == 0)
314 {
315 STAmount const tweakedTakerPays =
316 reducedTakerPays + drops(1);
317 STAmount const tweakedRate =
318 Quality(Amounts{tweakedTakerPays, reducedTakerGets})
319 .rate();
320 BEAST_EXPECT(tweakedRate > initialRate);
321 }
322#if 0
323 std::cout << "Placed rate: " << initialRate
324 << "; in-ledger rate: " << inLedgerRate
325 << "; TakerPays: " << reducedTakerPays
326 << "; TakerGets: " << reducedTakerGets
327 << "; alice already got: " << aliceGot
328 << std::endl;
329// #else
330 std::string_view filler = badRate ? "**" : " ";
331 std::cout << "| `" << reducedTakerGets << "` | `"
332 << reducedTakerPays << "` | `" << initialRate
333 << "` | " << filler << "`" << inLedgerRate << "`"
334 << filler << " | `" << aliceGot << "` |"
335 << std::endl;
336#endif
337 }
338
339 // In preparation for the next iteration make sure the two
340 // offers are gone from the ledger.
342 env, {{alice, aliceOfferSeq}, {bob, bobOfferSeq}});
343 return badRate;
344 };
345
346 // alice's offer (the old offer) is the same every time:
347 Amounts const aliceOffer{
348 STAmount(XRP(1)), STAmount(USD.issue(), 1, 0)};
349
350 // bob's offer has a slightly smaller TakerPays with each iteration.
351 // This should mean that the size of the offer alice leaves in the
352 // ledger should increase with each iteration.
353 unsigned int blockedCount = 0;
354 for (std::uint64_t mantissaReduce = 1'000'000'000ull;
355 mantissaReduce <= 4'000'000'000ull;
356 mantissaReduce += 20'000'000ull)
357 {
358 STAmount bobUSD{
359 aliceOffer.out.issue(),
360 aliceOffer.out.mantissa() - mantissaReduce,
361 aliceOffer.out.exponent()};
362 STAmount bobXRP{
363 aliceOffer.in.issue(), aliceOffer.in.mantissa() - 1};
364 Amounts bobsOffer{bobUSD, bobXRP};
365
366 blockedCount += exerciseOfferPair(aliceOffer, bobsOffer);
367 }
368
369 // If fixReducedOffersV1 is enabled, then none of the test cases
370 // should produce a potentially blocking rate.
371 //
372 // Also verify that if fixReducedOffersV1 is not enabled then
373 // some of the test cases produced a potentially blocking rate.
374 if (features[fixReducedOffersV1])
375 {
376 BEAST_EXPECT(blockedCount == 0);
377 }
378 else
379 {
380 BEAST_EXPECT(blockedCount > 10);
381 }
382 }
383 }
384
385 void
387 {
388 testcase("exercise underfunded XRP/IOU offer Q change");
389
390 // Bob places an offer that is not fully funded.
391 //
392 // This unit test compares the behavior of this situation before and
393 // after applying the fixReducedOffersV1 amendment.
394
395 using namespace jtx;
396 auto const alice = Account{"alice"};
397 auto const bob = Account{"bob"};
398 auto const gw = Account{"gw"};
399 auto const USD = gw["USD"];
400
401 // Make one test run without fixReducedOffersV1 and one with.
402 for (FeatureBitset features :
403 {supported_amendments() - fixReducedOffersV1,
404 supported_amendments() | fixReducedOffersV1})
405 {
406 Env env{*this, features};
407
408 env.fund(XRP(10000), alice, bob, gw);
409 env.close();
410 env.trust(USD(1000), alice, bob);
411
412 int blockedOrderBookCount = 0;
413 for (STAmount initialBobUSD = USD(0.45); initialBobUSD <= USD(1);
414 initialBobUSD += USD(0.025))
415 {
416 // underfund bob's offer
417 env(pay(gw, bob, initialBobUSD));
418 env.close();
419
420 std::uint32_t const bobOfferSeq = env.seq(bob);
421 env(offer(bob, drops(2), USD(1)));
422 env.close();
423
424 // alice places an offer that would cross bob's if bob's were
425 // well funded.
426 std::uint32_t const aliceOfferSeq = env.seq(alice);
427 env(offer(alice, USD(1), drops(2)));
428 env.close();
429
430 // We want to detect order book blocking. If:
431 // 1. bob's offer is still in the ledger and
432 // 2. alice received no USD
433 // then we use that as evidence that bob's offer blocked the
434 // order book.
435 {
436 bool const bobsOfferGone =
437 !offerInLedger(env, bob, bobOfferSeq);
438 STAmount const aliceBalanceUSD = env.balance(alice, USD);
439
440 // Sanity check the ledger if alice got USD.
441 if (aliceBalanceUSD.signum() > 0)
442 {
443 BEAST_EXPECT(aliceBalanceUSD == initialBobUSD);
444 BEAST_EXPECT(env.balance(bob, USD) == USD(0));
445 BEAST_EXPECT(bobsOfferGone);
446 }
447
448 // Track occurrences of order book blocking.
449 if (!bobsOfferGone && aliceBalanceUSD.signum() == 0)
450 {
451 ++blockedOrderBookCount;
452 }
453
454 // In preparation for the next iteration clean up any
455 // leftover offers.
457 env, {{alice, aliceOfferSeq}, {bob, bobOfferSeq}});
458
459 // Zero out alice's and bob's USD balances.
460 if (STAmount const aliceBalance = env.balance(alice, USD);
461 aliceBalance.signum() > 0)
462 env(pay(alice, gw, aliceBalance));
463
464 if (STAmount const bobBalance = env.balance(bob, USD);
465 bobBalance.signum() > 0)
466 env(pay(bob, gw, bobBalance));
467
468 env.close();
469 }
470 }
471
472 // If fixReducedOffersV1 is enabled, then none of the test cases
473 // should produce a potentially blocking rate.
474 //
475 // Also verify that if fixReducedOffersV1 is not enabled then
476 // some of the test cases produced a potentially blocking rate.
477 if (features[fixReducedOffersV1])
478 {
479 BEAST_EXPECT(blockedOrderBookCount == 0);
480 }
481 else
482 {
483 BEAST_EXPECT(blockedOrderBookCount > 15);
484 }
485 }
486 }
487
488 void
490 {
491 testcase("exercise underfunded IOU/IOU offer Q change");
492
493 // Bob places an IOU/IOU offer that is not fully funded.
494 //
495 // This unit test compares the behavior of this situation before and
496 // after applying the fixReducedOffersV1 amendment.
497
498 using namespace jtx;
499 using namespace std::chrono_literals;
500 auto const alice = Account{"alice"};
501 auto const bob = Account{"bob"};
502 auto const gw = Account{"gw"};
503
504 auto const USD = gw["USD"];
505 auto const EUR = gw["EUR"];
506
507 STAmount const tinyUSD(USD.issue(), /*mantissa*/ 1, /*exponent*/ -81);
508
509 // Make one test run without fixReducedOffersV1 and one with.
510 for (FeatureBitset features :
511 {supported_amendments() - fixReducedOffersV1,
512 supported_amendments() | fixReducedOffersV1})
513 {
514 Env env{*this, features};
515
516 env.fund(XRP(10000), alice, bob, gw);
517 env.close();
518 env.trust(USD(1000), alice, bob);
519 env.trust(EUR(1000), alice, bob);
520
521 STAmount const eurOffer(
522 EUR.issue(), /*mantissa*/ 2957, /*exponent*/ -76);
523 STAmount const usdOffer(
524 USD.issue(), /*mantissa*/ 7109, /*exponent*/ -76);
525
526 STAmount const endLoop(
527 USD.issue(), /*mantissa*/ 50, /*exponent*/ -81);
528
529 int blockedOrderBookCount = 0;
530 for (STAmount initialBobUSD = tinyUSD; initialBobUSD <= endLoop;
531 initialBobUSD += tinyUSD)
532 {
533 // underfund bob's offer
534 env(pay(gw, bob, initialBobUSD));
535 env(pay(gw, alice, EUR(100)));
536 env.close();
537
538 // This offer is underfunded
539 std::uint32_t bobOfferSeq = env.seq(bob);
540 env(offer(bob, eurOffer, usdOffer));
541 env.close();
542 env.require(offers(bob, 1));
543
544 // alice places an offer that crosses bob's.
545 std::uint32_t aliceOfferSeq = env.seq(alice);
546 env(offer(alice, usdOffer, eurOffer));
547 env.close();
548
549 // Examine the aftermath of alice's offer.
550 {
551 bool const bobsOfferGone =
552 !offerInLedger(env, bob, bobOfferSeq);
553 STAmount aliceBalanceUSD = env.balance(alice, USD);
554#if 0
556 << "bobs initial: " << initialBobUSD
557 << "; alice final: " << aliceBalanceUSD
558 << "; bobs offer: " << bobsOfferJson.toStyledString()
559 << std::endl;
560#endif
561 // Sanity check the ledger if alice got USD.
562 if (aliceBalanceUSD.signum() > 0)
563 {
564 BEAST_EXPECT(aliceBalanceUSD == initialBobUSD);
565 BEAST_EXPECT(env.balance(bob, USD) == USD(0));
566 BEAST_EXPECT(bobsOfferGone);
567 }
568
569 // Track occurrences of order book blocking.
570 if (!bobsOfferGone && aliceBalanceUSD.signum() == 0)
571 {
572 ++blockedOrderBookCount;
573 }
574 }
575
576 // In preparation for the next iteration clean up any
577 // leftover offers.
579 env, {{alice, aliceOfferSeq}, {bob, bobOfferSeq}});
580
581 // Zero out alice's and bob's IOU balances.
582 auto zeroBalance = [&env, &gw](
583 Account const& acct, IOU const& iou) {
584 if (STAmount const balance = env.balance(acct, iou);
585 balance.signum() > 0)
586 env(pay(acct, gw, balance));
587 };
588
589 zeroBalance(alice, EUR);
590 zeroBalance(alice, USD);
591 zeroBalance(bob, EUR);
592 zeroBalance(bob, USD);
593 env.close();
594 }
595
596 // If fixReducedOffersV1 is enabled, then none of the test cases
597 // should produce a potentially blocking rate.
598 //
599 // Also verify that if fixReducedOffersV1 is not enabled then
600 // some of the test cases produced a potentially blocking rate.
601 if (features[fixReducedOffersV1])
602 {
603 BEAST_EXPECT(blockedOrderBookCount == 0);
604 }
605 else
606 {
607 BEAST_EXPECT(blockedOrderBookCount > 20);
608 }
609 }
610 }
611
612 Amounts
614 {
615 STAmount const in =
616 amountFromJson(sfTakerPays, json[sfTakerPays.jsonName]);
617 STAmount const out =
618 amountFromJson(sfTakerGets, json[sfTakerGets.jsonName]);
619 return {in, out};
620 }
621
622 void
624 {
625 // This test case was motivated by Issue #4937. It recreates
626 // the specific failure identified in that issue and samples some other
627 // cases in the same vicinity to make sure that the new behavior makes
628 // sense.
629 testcase("exercise tfSell partial cross old XRP/IOU offer Q change");
630
631 using namespace jtx;
632
633 Account const gw("gateway");
634 Account const alice("alice");
635 Account const bob("bob");
636 Account const carol("carol");
637 auto const USD = gw["USD"];
638
639 // Make one test run without fixReducedOffersV2 and one with.
640 for (FeatureBitset features :
641 {supported_amendments() - fixReducedOffersV2,
642 supported_amendments() | fixReducedOffersV2})
643 {
644 // Make sure none of the offers we generate are under funded.
645 Env env{*this, features};
646 env.fund(XRP(10'000'000), gw, alice, bob, carol);
647 env.close();
648
649 env(trust(alice, USD(10'000'000)));
650 env(trust(bob, USD(10'000'000)));
651 env(trust(carol, USD(10'000'000)));
652 env.close();
653
654 env(pay(gw, alice, USD(10'000'000)));
655 env(pay(gw, bob, USD(10'000'000)));
656 env(pay(gw, carol, USD(10'000'000)));
657 env.close();
658
659 // Lambda that:
660 // 1. Exercises one offer trio,
661 // 2. Collects the results, and
662 // 3. Cleans up for the next offer trio.
663 auto exerciseOfferTrio =
664 [this, &env, &alice, &bob, &carol, &USD](
665 Amounts const& carolOffer) -> unsigned int {
666 // alice submits an offer that may become a blocker.
667 std::uint32_t const aliceOfferSeq = env.seq(alice);
668 static Amounts const aliceInitialOffer(USD(2), drops(3382562));
669 env(offer(alice, aliceInitialOffer.in, aliceInitialOffer.out));
670 env.close();
671 STAmount const initialRate =
673 env, alice, aliceOfferSeq)[jss::node]))
674 .rate();
675
676 // bob submits an offer that is more desirable than alice's
677 std::uint32_t const bobOfferSeq = env.seq(bob);
678 env(offer(bob, USD(0.97086565812384), drops(1642020)));
679 env.close();
680
681 // Now carol's offer consumes bob's and partially crosses
682 // alice's. The tfSell flag is important.
683 std::uint32_t const carolOfferSeq = env.seq(carol);
684 env(offer(carol, carolOffer.in, carolOffer.out),
685 txflags(tfSell));
686 env.close();
687
688 // carol's offer should not have made it into the ledger and
689 // bob's offer should be fully consumed.
690 if (!BEAST_EXPECT(
691 !offerInLedger(env, carol, carolOfferSeq) &&
692 !offerInLedger(env, bob, bobOfferSeq)))
693 {
694 // If carol's or bob's offers are still in the ledger then
695 // further results are meaningless.
697 env,
698 {{alice, aliceOfferSeq},
699 {bob, bobOfferSeq},
700 {carol, carolOfferSeq}});
701 return 1;
702 }
703 // alice's offer should still be in the ledger, but reduced in
704 // size.
705 unsigned int badRate = 1;
706 {
707 Json::Value aliceOffer =
708 ledgerEntryOffer(env, alice, aliceOfferSeq);
709
710 Amounts aliceReducedOffer =
711 jsonOfferToAmounts(aliceOffer[jss::node]);
712
713 BEAST_EXPECT(aliceReducedOffer.in < aliceInitialOffer.in);
714 BEAST_EXPECT(aliceReducedOffer.out < aliceInitialOffer.out);
715 STAmount const inLedgerRate =
716 Quality(aliceReducedOffer).rate();
717 badRate = inLedgerRate > initialRate ? 1 : 0;
718
719 // If the inLedgerRate is less than initial rate, then
720 // incrementing the mantissa of the reduced TakerGets
721 // should result in a rate higher than initial. Check
722 // this to verify that the largest allowable TakerGets
723 // was computed.
724 if (badRate == 0)
725 {
726 STAmount const tweakedTakerGets(
727 aliceReducedOffer.in.issue(),
728 aliceReducedOffer.in.mantissa() + 1,
729 aliceReducedOffer.in.exponent(),
730 aliceReducedOffer.in.negative());
731 STAmount const tweakedRate =
732 Quality(
733 Amounts{aliceReducedOffer.in, tweakedTakerGets})
734 .rate();
735 BEAST_EXPECT(tweakedRate > initialRate);
736 }
737#if 0
738 std::cout << "Placed rate: " << initialRate
739 << "; in-ledger rate: " << inLedgerRate
740 << "; TakerPays: " << aliceReducedOffer.in
741 << "; TakerGets: " << aliceReducedOffer.out
742 << std::endl;
743// #else
744 std::string_view filler = badRate ? "**" : " ";
745 std::cout << "| " << aliceReducedOffer.in << "` | `"
746 << aliceReducedOffer.out << "` | `" << initialRate
747 << "` | " << filler << "`" << inLedgerRate << "`"
748 << filler << std::endl;
749#endif
750 }
751
752 // In preparation for the next iteration make sure all three
753 // offers are gone from the ledger.
755 env,
756 {{alice, aliceOfferSeq},
757 {bob, bobOfferSeq},
758 {carol, carolOfferSeq}});
759 return badRate;
760 };
761
762 constexpr int loopCount = 100;
763 unsigned int blockedCount = 0;
764 {
765 STAmount increaseGets = USD(0);
766 STAmount const step(increaseGets.issue(), 1, -8);
767 for (unsigned int i = 0; i < loopCount; ++i)
768 {
769 blockedCount += exerciseOfferTrio(
770 Amounts(drops(1642020), USD(1) + increaseGets));
771 increaseGets += step;
772 }
773 }
774
775 // If fixReducedOffersV2 is enabled, then none of the test cases
776 // should produce a potentially blocking rate.
777 //
778 // Also verify that if fixReducedOffersV2 is not enabled then
779 // some of the test cases produced a potentially blocking rate.
780 if (features[fixReducedOffersV2])
781 {
782 BEAST_EXPECT(blockedCount == 0);
783 }
784 else
785 {
786 BEAST_EXPECT(blockedCount > 80);
787 }
788 }
789 }
790
791 void
792 run() override
793 {
799 }
800};
801
802BEAST_DEFINE_TESTSUITE_PRIO(ReducedOffer, tx, ripple, 2);
803
804} // namespace test
805} // namespace ripple
Represents a JSON value.
Definition: json_value.h:148
std::string asString() const
Returns the unquoted string value.
Definition: json_value.cpp:475
bool isMember(const char *key) const
Return true if the object has a member named key.
Definition: json_value.cpp:949
A testsuite class.
Definition: suite.h:55
testcase_t testcase
Memberspace for declaring test cases.
Definition: suite.h:155
int signum() const noexcept
Definition: STAmount.h:505
Issue const & issue() const
Definition: STAmount.h:487
void run() override
Runs the suite.
static void cleanupOldOffers(jtx::Env &env, std::initializer_list< std::pair< jtx::Account const &, std::uint32_t > > list)
static bool offerInLedger(jtx::Env &env, jtx::Account const &acct, std::uint32_t offerSeq)
static auto ledgerEntryOffer(jtx::Env &env, jtx::Account const &acct, std::uint32_t offer_seq)
Amounts jsonOfferToAmounts(Json::Value const &json)
Immutable cryptographic account descriptor.
Definition: Account.h:39
std::string const & human() const
Returns the human readable public key.
Definition: Account.h:114
A transaction testing environment.
Definition: Env.h:118
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition: Env.cpp:115
Json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition: Env.h:765
Converts to IOU Issue or STAmount.
A balance matches.
Definition: balance.h:39
balance(Account const &account, none_t)
Definition: balance.h:46
Set the fee on a JTx.
Definition: fee.h:36
Inject raw JSON.
Definition: jtx_json.h:32
Set the flags on a JTx.
Definition: txflags.h:31
T endl(T... args)
owner_count< ltOFFER > offers
Match the number of offers in the account's owner directory.
Definition: owners.h:90
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
Json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition: trust.cpp:31
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition: pay.cpp:29
Json::Value rate(Account const &account, double multiplier)
Set a transfer rate.
Definition: rate.cpp:31
Json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition: offer.cpp:28
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition: amount.cpp:104
FeatureBitset supported_amendments()
Definition: Env.h:71
Json::Value offer_cancel(Account const &account, std::uint32_t offerSeq)
Cancel an offer.
Definition: offer.cpp:45
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: algorithm.h:26
STAmount amountFromJson(SField const &name, Json::Value const &v)
Definition: STAmount.cpp:932
std::string to_string(base_uint< Bits, Tag > const &a)
Definition: base_uint.h:630
constexpr std::uint32_t tfSell
Definition: TxFlags.h:99