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