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#include <xrpld/ledger/PaymentSandbox.h>
28
29#include <xrpl/basics/contract.h>
30#include <xrpl/basics/random.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()),
72 std::nullopt,
73 std::nullopt);
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 sb.rules().enabled(featureOwnerPaysFee),
269 ammContext,
270 dummyJ);
271
272 BEAST_EXPECT(sr.first == tesSUCCESS);
273
274 if (sr.first != tesSUCCESS)
275 return;
276
277 // Due to the floating point calculations, theoretical and actual
278 // qualities are not expected to always be exactly equal. However, they
279 // should always be very close. This function checks that that two
280 // qualities are "close enough".
281 auto compareClose = [](Quality const& q1, Quality const& q2) {
282 // relative diff is fabs(a-b)/min(a,b)
283 // can't get access to internal value. Use the rate
284 constexpr double tolerance = 0.0000001;
285 return relativeDistance(q1, q2) <= tolerance;
286 };
287
288 for (auto const& strand : sr.second)
289 {
290 Quality const theoreticalQ = *qualityUpperBound(sb, strand);
291 auto const f = flow<IOUAmount, IOUAmount>(
292 sb, strand, IOUAmount(10, 0), IOUAmount(5, 0), dummyJ);
293 BEAST_EXPECT(f.success);
294 Quality const actualQ(f.out, f.in);
295 if (actualQ != theoreticalQ && !compareClose(actualQ, theoreticalQ))
296 {
297 BEAST_EXPECT(actualQ == theoreticalQ); // get the failure
298 log << "\nAcutal != Theoretical\n";
299 log << "\nTQ: " << prettyQuality(theoreticalQ) << "\n";
300 log << "AQ: " << prettyQuality(actualQ) << "\n";
301 logStrand(log, strand);
302 }
303 if (expectedQ && expectedQ != theoreticalQ &&
304 !compareClose(*expectedQ, theoreticalQ))
305 {
306 BEAST_EXPECT(expectedQ == theoreticalQ); // get the failure
307 log << "\nExpected != Theoretical\n";
308 log << "\nTQ: " << prettyQuality(theoreticalQ) << "\n";
309 log << "EQ: " << prettyQuality(*expectedQ) << "\n";
310 logStrand(log, strand);
311 }
312 };
313 }
314
315public:
316 void
317 testDirectStep(std::optional<int> const& reqNumIterations)
318 {
319 testcase("Direct Step");
320
321 // clang-format off
322
323 // Set up a payment through four accounts: alice -> bob -> carol -> dan
324 // For each relevant trust line on the path, there are three things that can vary:
325 // 1) input quality
326 // 2) output quality
327 // 3) debt direction
328 // For each account, there is one thing that can vary:
329 // 1) transfer rate
330
331 // clang-format on
332
333 using namespace jtx;
334
335 auto const currency = to_currency("USD");
336
337 constexpr std::size_t const numAccounts = 4;
338
339 // There are three relevant trust lines: `alice->bob`, `bob->carol`, and
340 // `carol->dan`. There are four accounts. If we count the number of
341 // combinations of parameters where a parameter is changed from its
342 // default value, there are
343 // 2^(num_trust_lines*num_trust_qualities+numAccounts) combinations of
344 // values to test, or 2^13 combinations. Use this value to set the
345 // number of iterations. Note however that many of these parameter
346 // combinations run essentially the same test. For example, changing the
347 // quality values for bob and carol test almost the same thing.
348 // Similarly, changing the transfer rates on bob and carol test almost
349 // the same thing. Instead of systematically running these 8k tests,
350 // randomly sample the test space.
351 int const numTestIterations = reqNumIterations.value_or(250);
352
353 constexpr std::uint32_t paymentAmount = 1;
354
355 // Class to randomly set account transfer rates, qualities, and other
356 // params.
357 RandomAccountParams rndAccParams;
358
359 // Tests are sped up by a factor of 2 if a new environment isn't created
360 // on every iteration.
361 Env env(*this, supported_amendments());
362 for (int i = 0; i < numTestIterations; ++i)
363 {
364 auto const iterAsStr = std::to_string(i);
365 // New set of accounts on every iteration so the environment doesn't
366 // need to be recreated (2x speedup)
367 auto const alice = Account("alice" + iterAsStr);
368 auto const bob = Account("bob" + iterAsStr);
369 auto const carol = Account("carol" + iterAsStr);
370 auto const dan = Account("dan" + iterAsStr);
371 std::array<Account, numAccounts> accounts{{alice, bob, carol, dan}};
372 static_assert(
373 numAccounts == 4, "Path is only correct for four accounts");
374 path const accountsPath(accounts[1], accounts[2]);
375 env.fund(XRP(10000), alice, bob, carol, dan);
376 env.close();
377
378 // iterate through all pairs of accounts, randomly set the transfer
379 // rate, qIn, qOut, and if the account issues or redeems
380 for (std::size_t ii = 0; ii < numAccounts; ++ii)
381 {
382 rndAccParams.maybeSetTransferRate(env, accounts[ii]);
383 // The payment is from:
384 // account[0] -> account[1] -> account[2] -> account[3]
385 // set the trust lines and initial balances for each pair of
386 // neighboring accounts
387 std::size_t const j = ii + 1;
388 if (j == numAccounts)
389 continue;
390
391 rndAccParams.setupTrustLines(
392 env, accounts[ii], accounts[j], currency);
393 rndAccParams.maybeSetInitialBalance(
394 env, accounts[ii], accounts[j], currency);
395 }
396
397 // Accounts are set up, make the payment
398 IOU const iou{accounts.back(), currency};
400 pay(accounts.front(), accounts.back(), iou(paymentAmount)),
401 accountsPath,
403
404 testCase(rcp, env.closed());
405 }
406 }
407
408 void
409 testBookStep(std::optional<int> const& reqNumIterations)
410 {
411 testcase("Book Step");
412 using namespace jtx;
413
414 // clang-format off
415
416 // Setup a payment through an offer: alice (USD/bob) -> bob -> (USD/bob)|(EUR/carol) -> carol -> dan
417 // For each relevant trust line, vary input quality, output quality, debt direction.
418 // For each account, vary transfer rate.
419 // The USD/bob|EUR/carol offer owner is "Oscar"
420
421 // clang-format on
422
423 int const numTestIterations = reqNumIterations.value_or(100);
424
425 constexpr std::uint32_t paymentAmount = 1;
426
427 Currency const eurCurrency = to_currency("EUR");
428 Currency const usdCurrency = to_currency("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, supported_amendments());
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 constexpr std::size_t const numAccounts = 5;
449 {alice, bob, carol, dan, oscar}};
450
451 // sendmax should be in USDB and delivered amount should be in EURC
452 // normalized path should be:
453 // alice -> bob -> (USD/bob)|(EUR/carol) -> carol -> dan
454 path const bookPath(~EURC);
455
456 env.fund(XRP(10000), alice, bob, carol, dan, oscar);
457 env.close();
458
459 for (auto const& acc : accounts)
460 rndAccParams.maybeSetTransferRate(env, acc);
461
462 for (auto const& currency : {usdCurrency, eurCurrency})
463 {
464 rndAccParams.setupTrustLines(
465 env, alice, bob, currency); // first step in payment
466 rndAccParams.setupTrustLines(
467 env, carol, dan, currency); // last step in payment
468 rndAccParams.setupTrustLines(
469 env, oscar, bob, currency); // offer owner
470 rndAccParams.setupTrustLines(
471 env, oscar, carol, currency); // offer owner
472 }
473
474 rndAccParams.maybeSetInitialBalance(env, alice, bob, usdCurrency);
475 rndAccParams.maybeSetInitialBalance(env, carol, dan, eurCurrency);
476 rndAccParams.setInitialBalance(env, oscar, bob, usdCurrency);
477 rndAccParams.setInitialBalance(env, oscar, carol, eurCurrency);
478
479 env(offer(oscar, USDB(50), EURC(50)));
480 env.close();
481
482 // Accounts are set up, make the payment
483 IOU const srcIOU{bob, usdCurrency};
484 IOU const dstIOU{carol, eurCurrency};
486 pay(alice, dan, dstIOU(paymentAmount)),
487 sendmax(srcIOU(100 * paymentAmount)),
488 bookPath,
490
491 testCase(rcp, env.closed());
492 }
493 }
494
495 void
497 {
498 testcase("Relative quality distance");
499
500 auto toQuality = [](std::uint64_t mantissa,
501 int exponent = 0) -> Quality {
502 // The only way to construct a Quality from an STAmount is to take
503 // their ratio. Set the denominator STAmount to `one` to easily
504 // create a quality from a single amount
505 STAmount const one{noIssue(), 1};
506 STAmount const v{noIssue(), mantissa, exponent};
507 return Quality{one, v};
508 };
509
510 BEAST_EXPECT(relativeDistance(toQuality(100), toQuality(100)) == 0);
511 BEAST_EXPECT(relativeDistance(toQuality(100), toQuality(100, 1)) == 9);
512 BEAST_EXPECT(relativeDistance(toQuality(100), toQuality(110)) == .1);
513 BEAST_EXPECT(
514 relativeDistance(toQuality(100, 90), toQuality(110, 90)) == .1);
515 BEAST_EXPECT(
516 relativeDistance(toQuality(100, 90), toQuality(110, 91)) == 10);
517 BEAST_EXPECT(
518 relativeDistance(toQuality(100, 0), toQuality(100, 90)) == 1e90);
519 // Make the mantissa in the smaller value bigger than the mantissa in
520 // the larger value. Instead of checking the exact result, we check that
521 // it's large. If the values did not compare correctly in
522 // `relativeDistance`, then the returned value would be negative.
523 BEAST_EXPECT(
524 relativeDistance(toQuality(102, 0), toQuality(101, 90)) >= 1e89);
525 }
526
527 void
528 run() override
529 {
530 // Use the command line argument `--unittest-arg=500 ` to change the
531 // number of iterations to 500
532 auto const numIterations = [s = arg()]() -> std::optional<int> {
533 if (s.empty())
534 return std::nullopt;
535 try
536 {
537 std::size_t pos;
538 auto const r = stoi(s, &pos);
539 if (pos != s.size())
540 return std::nullopt;
541 return r;
542 }
543 catch (...)
544 {
545 return std::nullopt;
546 }
547 }();
549 testDirectStep(numIterations);
550 testBookStep(numIterations);
551 }
552};
553
554BEAST_DEFINE_TESTSUITE_PRIO(TheoreticalQuality, app, ripple, 3);
555
556} // namespace test
557} // namespace ripple
Represents a JSON value.
Definition: json_value.h:150
bool isMember(char const *key) const
Return true if the object has a member named key.
Definition: json_value.cpp:962
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:522
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition: Env.cpp:111
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition: Env.cpp:117
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition: Env.cpp:233
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)
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
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:105
FeatureBitset supported_amendments()
Definition: Env.h:74
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: algorithm.h:26
constexpr Number one
Definition: Number.cpp:175
bool isXRP(AccountID const &c)
Definition: AccountID.h:91
std::optional< AccountID > parseBase58(std::string const &s)
Parse AccountID from checked, base58 string.
Definition: AccountID.cpp:124
STAmount amountFromJson(SField const &name, Json::Value const &v)
Definition: STAmount.cpp:879
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, beast::Journal j)
Create a Strand for each specified path (including the default path, if indicated)
Definition: PaySteps.cpp:467
@ no
Definition: Steps.h:44
QualityDirection
Definition: Steps.h:42
constexpr std::uint32_t tfPartialPayment
Definition: TxFlags.h:105
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition: Issue.h:126
constexpr std::uint32_t tfNoRippleDirect
Definition: TxFlags.h:104
@ tesSUCCESS
Definition: TER.h:244
@ tapNONE
Definition: ApplyView.h:32
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)