xrpld
Loading...
Searching...
No Matches
TheoreticalQuality_test.cpp
1
2#include <test/jtx/Account.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/amount.h>
5#include <test/jtx/balance.h> // IWYU pragma: keep
6#include <test/jtx/offer.h>
7#include <test/jtx/paths.h>
8#include <test/jtx/pay.h>
9#include <test/jtx/rate.h>
10#include <test/jtx/sendmax.h>
11#include <test/jtx/trust.h>
12#include <test/jtx/txflags.h>
13
14#include <xrpl/beast/unit_test/suite.h>
15#include <xrpl/beast/utility/Journal.h>
16#include <xrpl/beast/xor_shift_engine.h>
17#include <xrpl/json/json_value.h>
18#include <xrpl/ledger/ApplyView.h>
19#include <xrpl/ledger/PaymentSandbox.h>
20#include <xrpl/protocol/AccountID.h>
21#include <xrpl/protocol/Asset.h>
22#include <xrpl/protocol/IOUAmount.h>
23#include <xrpl/protocol/Issue.h>
24#include <xrpl/protocol/Quality.h>
25#include <xrpl/protocol/SField.h>
26#include <xrpl/protocol/STAmount.h>
27#include <xrpl/protocol/STPathSet.h>
28#include <xrpl/protocol/TER.h>
29#include <xrpl/protocol/TxFlags.h>
30#include <xrpl/protocol/UintTypes.h>
31#include <xrpl/protocol/jss.h>
32#include <xrpl/tx/paths/detail/Steps.h>
33#include <xrpl/tx/paths/detail/StrandFlow.h>
34#include <xrpl/tx/transactors/dex/AMMContext.h>
35
36#include <array>
37#include <cassert>
38#include <cstddef>
39#include <cstdint>
40#include <memory>
41#include <optional>
42#include <random>
43#include <sstream>
44#include <string>
45#include <utility>
46
47namespace xrpl::test {
48
50{
53
56
58
60 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
61 : srcAccount{*parseBase58<AccountID>(jv[jss::Account].asString())}
62 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
63 , dstAccount{*parseBase58<AccountID>(jv[jss::Destination].asString())}
64 , dstAmt{amountFromJson(sfAmount, jv[jss::Amount])}
65 {
66 if (jv.isMember(jss::SendMax))
67 sendMax = amountFromJson(sfSendMax, jv[jss::SendMax]);
68
69 if (jv.isMember(jss::Paths))
70 {
71 // paths is an array of arrays
72 // each leaf element will be of the form
73 for (auto const& path : jv[jss::Paths])
74 {
75 STPath p;
76 for (auto const& pe : path)
77 {
78 if (pe.isMember(jss::account))
79 {
80 assert(!pe.isMember(jss::currency) && !pe.isMember(jss::issuer));
81 p.emplaceBack(
82 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
83 *parseBase58<AccountID>(pe[jss::account].asString()),
84 std::nullopt,
85 std::nullopt);
86 }
87 else if (pe.isMember(jss::currency) && pe.isMember(jss::issuer))
88 {
89 auto const currency = toCurrency(pe[jss::currency].asString());
90 std::optional<AccountID> issuer;
91 if (!isXRP(currency))
92 {
93 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
94 issuer = *parseBase58<AccountID>(pe[jss::issuer].asString());
95 }
96 else
97 {
98 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
99 assert(isXRP(*parseBase58<AccountID>(pe[jss::issuer].asString())));
100 }
101 p.emplaceBack(std::nullopt, currency, issuer);
102 }
103 else
104 {
105 assert(0);
106 }
107 }
108 paths.emplaceBack(std::move(p));
109 }
110 }
111 }
112};
113
114// Class to randomly set an account's transfer rate, quality in, quality out,
115// and initial balance
117{
120 // Balance to set if an account redeems into another account. Otherwise
121 // the balance will be zero. Since we are testing quality measures, the
122 // payment should not use multiple qualities, so the initialBalance
123 // needs to be able to handle an entire payment (otherwise an account
124 // will go from redeeming to issuing and the fees/qualities can change)
126
127 // probability of changing a value from its default
128 static constexpr double kProbChangeDefault = 0.75;
129 // probability that an account redeems into another account
130 static constexpr double kProbRedeem = 0.5;
134
135 bool
137 {
139 };
140
141 void
143 {
144 if (!shouldSet())
145 return;
146
147 auto const percent = qualityPercentDist_(engine_);
148 auto const& field = qDir == QualityDirection::In ? sfQualityIn : sfQualityOut;
149 auto const value = static_cast<std::uint32_t>((percent / 100) * QUALITY_ONE);
150 jv[field.jsonName] = value;
151 };
152
153 // Setup the trust amounts and in/out qualities (but not the balances)
154 void
156 jtx::Env& env,
157 jtx::Account const& acc,
158 jtx::Account const& peer,
159 Currency const& currency)
160 {
161 using namespace jtx;
162 IOU const iou{peer, currency};
163 json::Value jv = trust(acc, iou(trustAmount_));
166 env(jv);
167 env.close();
168 };
169
170public:
171 explicit RandomAccountParams(std::uint32_t trustAmount = 100, std::uint32_t initialBalance = 50)
172 // Use a deterministic seed so the unit tests run in a reproducible way
173 : engine_{1977u}, trustAmount_{trustAmount}, initialBalance_{initialBalance} {};
174
175 void
177 {
178 if (shouldSet())
179 env(rate(acc, transferRateDist_(engine_)));
180 }
181
182 // Set the initial balance, taking into account the qualities
183 void
185 jtx::Env& env,
186 jtx::Account const& acc,
187 jtx::Account const& peer,
188 Currency const& currency) const
189 {
190 using namespace jtx;
191 IOU const iou{acc, currency};
192 // This payment sets the acc's balance to `initialBalance`.
193 // Since input qualities complicate this payment, use `sendMax` with
194 // `initialBalance` to make sure the balance is set correctly.
195 env(pay(peer, acc, iou(trustAmount_)),
197 Txflags(tfPartialPayment));
198 env.close();
199 }
200
201 void
203 jtx::Env& env,
204 jtx::Account const& acc,
205 jtx::Account const& peer,
206 Currency const& currency)
207 {
208 using namespace jtx;
210 return;
211 setInitialBalance(env, acc, peer, currency);
212 }
213
214 // Setup the trust amounts and in/out qualities (but not the balances) on
215 // both sides of the trust line
216 void
218 jtx::Env& env,
219 jtx::Account const& acc1,
220 jtx::Account const& acc2,
221 Currency const& currency)
222 {
223 setupTrustLine(env, acc1, acc2, currency);
224 setupTrustLine(env, acc2, acc1, currency);
225 };
226};
227
229{
230 static std::string
232 {
234 STAmount const rate = q.rate();
235 sstr << rate << " (" << q << ")";
236 return sstr.str();
237 };
238
239 template <class Stream>
240 static void
241 logStrand(Stream& stream, Strand const& strand)
242 {
243 stream << "Strand:\n";
244 for (auto const& step : strand)
245 stream << "\n" << *step;
246 stream << "\n\n";
247 };
248
249 void
251 RippleCalcTestParams const& rcp,
253 std::optional<Quality> const& expectedQ = {})
254 {
255 PaymentSandbox const sb(closed.get(), TapNone);
256 AMMContext ammContext(rcp.srcAccount, false);
257
258 auto const sendMaxIssue = [&rcp]() -> std::optional<Asset> {
259 if (rcp.sendMax)
260 return rcp.sendMax->asset();
261 return std::nullopt;
262 }();
263
265
266 auto sr = toStrands(
267 sb,
268 rcp.srcAccount,
269 rcp.dstAccount,
270 rcp.dstAmt.asset(),
271 /*limitQuality*/ std::nullopt,
272 sendMaxIssue,
273 rcp.paths,
274 /*defaultPaths*/ rcp.paths.empty(),
275 false,
276 OfferCrossing::No,
277 ammContext,
278 std::nullopt,
279 dummyJ);
280
281 BEAST_EXPECT(isTesSuccess(sr.first));
282
283 if (!isTesSuccess(sr.first))
284 return;
285
286 // Due to the floating point calculations, theoretical and actual
287 // qualities are not expected to always be exactly equal. However, they
288 // should always be very close. This function checks that that two
289 // qualities are "close enough".
290 auto compareClose = [](Quality const& q1, Quality const& q2) {
291 // relative diff is fabs(a-b)/min(a,b)
292 // can't get access to internal value. Use the rate
293 static constexpr double kTolerance = 0.0000001;
294 return relativeDistance(q1, q2) <= kTolerance;
295 };
296
297 for (auto const& strand : sr.second)
298 {
299 Quality const theoreticalQ =
300 *qualityUpperBound(sb, strand); // NOLINT(bugprone-unchecked-optional-access)
301 auto const f =
302 flow<IOUAmount, IOUAmount>(sb, strand, IOUAmount(10, 0), IOUAmount(5, 0), dummyJ);
303 BEAST_EXPECT(f.success);
304 Quality const actualQ(f.out, f.in);
305 if (actualQ != theoreticalQ && !compareClose(actualQ, theoreticalQ))
306 {
307 BEAST_EXPECT(actualQ == theoreticalQ); // get the failure
308 log << "\nActual != Theoretical\n";
309 log << "\nTQ: " << prettyQuality(theoreticalQ) << "\n";
310 log << "AQ: " << prettyQuality(actualQ) << "\n";
311 logStrand(log, strand);
312 }
313 if (expectedQ && expectedQ != theoreticalQ && !compareClose(*expectedQ, theoreticalQ))
314 {
315 BEAST_EXPECT(expectedQ == theoreticalQ); // get the failure
316 log << "\nExpected != Theoretical\n";
317 log << "\nTQ: " << prettyQuality(theoreticalQ) << "\n";
318 log << "EQ: " << prettyQuality(*expectedQ) << "\n";
319 logStrand(log, strand);
320 }
321 };
322 }
323
324public:
325 void
326 testDirectStep(std::optional<int> const& reqNumIterations)
327 {
328 testcase("Direct Step");
329
330 // Set up a payment through four accounts: alice -> bob -> carol -> dan
331 // For each relevant trust line on the path, there are three things that can vary:
332 // 1) input quality
333 // 2) output quality
334 // 3) debt direction
335 // For each account, there is one thing that can vary:
336 // 1) transfer rate
337
338 using namespace jtx;
339
340 auto const currency = toCurrency("USD");
341
342 static constexpr std::size_t kNumAccounts = 4;
343
344 // There are three relevant trust lines: `alice->bob`, `bob->carol`, and
345 // `carol->dan`. There are four accounts. If we count the number of
346 // combinations of parameters where a parameter is changed from its
347 // default value, there are
348 // 2^(nutrust_lines_*nutrust_qualities_+numAccounts) combinations of
349 // values to test, or 2^13 combinations. Use this value to set the
350 // number of iterations. Note however that many of these parameter
351 // combinations run essentially the same test. For example, changing the
352 // quality values for bob and carol test almost the same thing.
353 // Similarly, changing the transfer rates on bob and carol test almost
354 // the same thing. Instead of systematically running these 8k tests,
355 // randomly sample the test space.
356 int const numTestIterations = reqNumIterations.value_or(250);
357
358 static constexpr std::uint32_t kPaymentAmount = 1;
359
360 // Class to randomly set account transfer rates, qualities, and other
361 // params.
362 RandomAccountParams rndAccParams;
363
364 // Tests are sped up by a factor of 2 if a new environment isn't created
365 // on every iteration.
366 Env env(*this, testableAmendments());
367 for (int i = 0; i < numTestIterations; ++i)
368 {
369 auto const iterAsStr = std::to_string(i);
370 // New set of accounts on every iteration so the environment doesn't
371 // need to be recreated (2x speedup)
372 auto const alice = Account("alice" + iterAsStr);
373 auto const bob = Account("bob" + iterAsStr);
374 auto const carol = Account("carol" + iterAsStr);
375 auto const dan = Account("dan" + iterAsStr);
376 std::array<Account, kNumAccounts> accounts{{alice, bob, carol, dan}};
377 static_assert(kNumAccounts == 4, "Path is only correct for four accounts");
378 Path const accountsPath(accounts[1], accounts[2]);
379 env.fund(XRP(10000), alice, bob, carol, dan);
380 env.close();
381
382 // iterate through all pairs of accounts, randomly set the transfer
383 // rate, qIn, qOut, and if the account issues or redeems
384 for (std::size_t ii = 0; ii < kNumAccounts; ++ii)
385 {
386 rndAccParams.maybeSetTransferRate(env, accounts[ii]);
387 // The payment is from:
388 // account[0] -> account[1] -> account[2] -> account[3]
389 // set the trust lines and initial balances for each pair of
390 // neighboring accounts
391 std::size_t const j = ii + 1;
392 if (j == kNumAccounts)
393 continue;
394
395 rndAccParams.setupTrustLines(env, accounts[ii], accounts[j], currency);
396 rndAccParams.maybeSetInitialBalance(env, accounts[ii], accounts[j], currency);
397 }
398
399 // Accounts are set up, make the payment
400 IOU const iou{accounts.back(), currency};
401 RippleCalcTestParams const rcp{env.json(
402 pay(accounts.front(), accounts.back(), iou(kPaymentAmount)),
403 accountsPath,
404 Txflags(tfNoRippleDirect))};
405
406 testCase(rcp, env.closed());
407 }
408 }
409
410 void
411 testBookStep(std::optional<int> const& reqNumIterations)
412 {
413 testcase("Book Step");
414 using namespace jtx;
415
416 // Setup a payment through an offer:
417 // alice (USD/bob) -> bob -> (USD/bob)|(EUR/carol) -> carol -> dan
418 // For each relevant trust line, vary input quality, output quality, debt direction. For
419 // each account, vary transfer rate.
420
421 // The USD/bob|EUR/carol offer owner is "Oscar".
422
423 int const numTestIterations = reqNumIterations.value_or(100);
424
425 static constexpr std::uint32_t kPaymentAmount = 1;
426
427 Currency const eurCurrency = toCurrency("EUR");
428 Currency const usdCurrency = toCurrency("USD");
429
430 // Class to randomly set account transfer rates, qualities, and other
431 // params.
432 RandomAccountParams rndAccParams;
433
434 // Speed up tests by creating the environment outside the loop
435 // (factor of 2 speedup on the DirectStep tests)
436 Env env(*this, testableAmendments());
437 for (int i = 0; i < numTestIterations; ++i)
438 {
439 auto const iterAsStr = std::to_string(i);
440 auto const alice = Account("alice" + iterAsStr);
441 auto const bob = Account("bob" + iterAsStr);
442 auto const carol = Account("carol" + iterAsStr);
443 auto const dan = Account("dan" + iterAsStr);
444 auto const oscar = Account("oscar" + iterAsStr); // offer owner
445 auto const usdb = bob["USD"];
446 auto const eurc = carol["EUR"];
447 static constexpr std::size_t kNumAccounts = 5;
448 std::array<Account, kNumAccounts> const accounts{{alice, bob, carol, dan, oscar}};
449
450 // sendmax should be in USDB and delivered amount should be in EURC
451 // normalized path should be:
452 // alice -> bob -> (USD/bob)|(EUR/carol) -> carol -> dan
453 Path const bookPath(~eurc);
454
455 env.fund(XRP(10000), alice, bob, carol, dan, oscar);
456 env.close();
457
458 for (auto const& acc : accounts)
459 rndAccParams.maybeSetTransferRate(env, acc);
460
461 for (auto const& currency : {usdCurrency, eurCurrency})
462 {
463 rndAccParams.setupTrustLines(env, alice, bob, currency); // first step in payment
464 rndAccParams.setupTrustLines(env, carol, dan, currency); // last step in payment
465 rndAccParams.setupTrustLines(env, oscar, bob, currency); // offer owner
466 rndAccParams.setupTrustLines(env, oscar, carol, currency); // offer owner
467 }
468
469 rndAccParams.maybeSetInitialBalance(env, alice, bob, usdCurrency);
470 rndAccParams.maybeSetInitialBalance(env, carol, dan, eurCurrency);
471 rndAccParams.setInitialBalance(env, oscar, bob, usdCurrency);
472 rndAccParams.setInitialBalance(env, oscar, carol, eurCurrency);
473
474 env(offer(oscar, usdb(50), eurc(50)));
475 env.close();
476
477 // Accounts are set up, make the payment
478 IOU const srcIOU{bob, usdCurrency};
479 IOU const dstIOU{carol, eurCurrency};
480 RippleCalcTestParams const rcp{env.json(
481 pay(alice, dan, dstIOU(kPaymentAmount)),
482 Sendmax(srcIOU(100 * kPaymentAmount)),
483 bookPath,
484 Txflags(tfNoRippleDirect))};
485
486 testCase(rcp, env.closed());
487 }
488 }
489
490 void
492 {
493 testcase("Relative quality distance");
494
495 auto toQuality = [](std::uint64_t mantissa, int exponent = 0) -> Quality {
496 // The only way to construct a Quality from an STAmount is to take
497 // their ratio. Set the denominator STAmount to `one` to easily
498 // create a quality from a single amount
499 STAmount const one{noIssue(), 1};
500 STAmount const v{noIssue(), mantissa, exponent};
501 return Quality{one, v};
502 };
503
504 BEAST_EXPECT(relativeDistance(toQuality(100), toQuality(100)) == 0);
505 BEAST_EXPECT(relativeDistance(toQuality(100), toQuality(100, 1)) == 9);
506 BEAST_EXPECT(relativeDistance(toQuality(100), toQuality(110)) == .1);
507 BEAST_EXPECT(relativeDistance(toQuality(100, 90), toQuality(110, 90)) == .1);
508 BEAST_EXPECT(relativeDistance(toQuality(100, 90), toQuality(110, 91)) == 10);
509 BEAST_EXPECT(relativeDistance(toQuality(100, 0), toQuality(100, 90)) == 1e90);
510 // Make the mantissa in the smaller value bigger than the mantissa in
511 // the larger value. Instead of checking the exact result, we check that
512 // it's large. If the values did not compare correctly in
513 // `relativeDistance`, then the returned value would be negative.
514 BEAST_EXPECT(relativeDistance(toQuality(102, 0), toQuality(101, 90)) >= 1e89);
515 }
516
517 void
518 run() override
519 {
520 // Use the command line argument `--unittest-arg=500 ` to change the
521 // number of iterations to 500
522 auto const numIterations = [s = arg()]() -> std::optional<int> {
523 if (s.empty())
524 return std::nullopt;
525 try
526 {
527 std::size_t pos = 0;
528 auto const r = stoi(s, &pos);
529 if (pos != s.size())
530 return std::nullopt;
531 return r;
532 }
533 catch (...)
534 {
535 return std::nullopt;
536 }
537 }();
539 testDirectStep(numIterations);
540 testBookStep(numIterations);
541 }
542};
543
544BEAST_DEFINE_TESTSUITE_PRIO(TheoreticalQuality, app, xrpl, 3);
545
546} // namespace xrpl::test
T back(T... args)
A generic endpoint for log messages.
Definition Journal.h:38
static Sink & getNullSink()
Returns a Sink which does nothing.
A testsuite class.
Definition suite.h:50
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
std::string const & arg() const
Return the argument associated with the runner.
Definition suite.h:278
Represents a JSON value.
Definition json_value.h:130
bool isMember(char const *key) const
Return true if the object has a member named key.
Maintains AMM info per overall payment engine execution and individual iteration.
Definition AMMContext.h:16
A wrapper which makes credits unavailable to balances.
Represents the logical ratio of output currency to input currency.
Definition Quality.h:91
STAmount rate() const
Returns the quality as STAmount.
Definition Quality.h:149
Asset const & asset() const
Definition STAmount.h:478
bool empty() const
Definition STPathSet.h:534
void setupTrustLine(jtx::Env &env, jtx::Account const &acc, jtx::Account const &peer, Currency const &currency)
std::uniform_real_distribution qualityPercentDist_
void setupTrustLines(jtx::Env &env, jtx::Account const &acc1, jtx::Account const &acc2, Currency const &currency)
RandomAccountParams(std::uint32_t trustAmount=100, std::uint32_t initialBalance=50)
std::uniform_real_distribution transferRateDist_
void setInitialBalance(jtx::Env &env, jtx::Account const &acc, jtx::Account const &peer, Currency const &currency) const
void maybeInsertQuality(json::Value &jv, QualityDirection qDir)
void maybeSetInitialBalance(jtx::Env &env, jtx::Account const &acc, jtx::Account const &peer, Currency const &currency)
void maybeSetTransferRate(jtx::Env &env, jtx::Account const &acc)
std::uniform_real_distribution zeroOneDist_
static std::string prettyQuality(Quality const &q)
void testDirectStep(std::optional< int > const &reqNumIterations)
void testCase(RippleCalcTestParams const &rcp, std::shared_ptr< ReadView const > closed, std::optional< Quality > const &expectedQ={})
void testBookStep(std::optional< int > const &reqNumIterations)
static void logStrand(Stream &stream, Strand const &strand)
Immutable cryptographic account descriptor.
Definition jtx/Account.h:17
A transaction testing environment.
Definition Env.h:143
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:592
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:127
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:296
Converts to IOU Issue or STAmount.
Add a path.
Definition paths.h:39
Sets the SendMax on a JTx.
Definition sendmax.h:13
Set the flags on a JTx.
Definition txflags.h:9
T front(T... args)
T get(T... args)
T log(T... args)
detail::XorShiftEngine<> xor_shift_engine
XOR-shift Generator.
Definition jss.h:5
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
FeatureBitset testableAmendments()
Definition Env.h:76
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
json::Value rate(Account const &account, double multiplier)
Set a transfer rate.
Definition rate.cpp:15
BEAST_DEFINE_TESTSUITE_PRIO(AccountDelete, app, xrpl, 2)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
std::optional< AccountID > parseBase58(std::string const &s)
Parse AccountID from checked, base58 string.
BaseUInt< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition UintTypes.h:36
bool toCurrency(Currency &, std::string const &)
Tries to convert a string to a Currency, returns true on success.
Definition UintTypes.cpp:65
QualityDirection
Definition Steps.h:22
STAmount amountFromJson(SField const &name, json::Value const &v)
Definition STAmount.cpp:916
@ TapNone
Definition ApplyView.h:13
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition Issue.h:105
T str(T... args)
T to_string(T... args)
T value_or(T... args)