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#include <xrpld/app/paths/AMMContext.h>
23#include <xrpld/app/paths/Flow.h>
24#include <xrpld/app/paths/detail/Steps.h>
25#include <xrpld/app/paths/detail/StrandFlow.h>
26#include <xrpld/core/Config.h>
27#include <xrpld/ledger/ApplyViewImpl.h>
28#include <xrpld/ledger/PaymentSandbox.h>
29#include <xrpld/ledger/Sandbox.h>
30#include <xrpl/basics/contract.h>
31#include <xrpl/basics/random.h>
32#include <xrpl/protocol/Feature.h>
33#include <xrpl/protocol/jss.h>
34
35namespace ripple {
36namespace test {
37
39{
42
45
47
49 : srcAccount{*parseBase58<AccountID>(jv[jss::Account].asString())}
50 , dstAccount{*parseBase58<AccountID>(jv[jss::Destination].asString())}
51 , dstAmt{amountFromJson(sfAmount, jv[jss::Amount])}
52 {
53 if (jv.isMember(jss::SendMax))
54 sendMax = amountFromJson(sfSendMax, jv[jss::SendMax]);
55
56 if (jv.isMember(jss::Paths))
57 {
58 // paths is an array of arrays
59 // each leaf element will be of the form
60 for (auto const& path : jv[jss::Paths])
61 {
62 STPath p;
63 for (auto const& pe : path)
64 {
65 if (pe.isMember(jss::account))
66 {
67 assert(
68 !pe.isMember(jss::currency) &&
69 !pe.isMember(jss::issuer));
71 *parseBase58<AccountID>(
72 pe[jss::account].asString()),
73 std::nullopt,
74 std::nullopt);
75 }
76 else if (
77 pe.isMember(jss::currency) && pe.isMember(jss::issuer))
78 {
79 auto const currency =
80 to_currency(pe[jss::currency].asString());
82 if (!isXRP(currency))
83 issuer = *parseBase58<AccountID>(
84 pe[jss::issuer].asString());
85 else
86 assert(isXRP(*parseBase58<AccountID>(
87 pe[jss::issuer].asString())));
88 p.emplace_back(std::nullopt, currency, issuer);
89 }
90 else
91 {
92 assert(0);
93 }
94 }
95 paths.emplace_back(std::move(p));
96 }
97 }
98 }
99};
100
101// Class to randomly set an account's transfer rate, quality in, quality out,
102// and initial balance
104{
107 // Balance to set if an account redeems into another account. Otherwise
108 // the balance will be zero. Since we are testing quality measures, the
109 // payment should not use multiple qualities, so the initialBalance
110 // needs to be able to handle an entire payment (otherwise an account
111 // will go from redeeming to issuing and the fees/qualities can change)
113
114 // probability of changing a value from its default
115 constexpr static double probChangeDefault_ = 0.75;
116 // probability that an account redeems into another account
117 constexpr static double probRedeem_ = 0.5;
121
122 bool
124 {
126 };
127
128 void
130 {
131 if (!shouldSet())
132 return;
133
134 auto const percent = qualityPercentDist_(engine_);
135 auto const& field =
136 qDir == QualityDirection::in ? sfQualityIn : sfQualityOut;
137 auto const value =
138 static_cast<std::uint32_t>((percent / 100) * QUALITY_ONE);
139 jv[field.jsonName] = value;
140 };
141
142 // Setup the trust amounts and in/out qualities (but not the balances)
143 void
145 jtx::Env& env,
146 jtx::Account const& acc,
147 jtx::Account const& peer,
148 Currency const& currency)
149 {
150 using namespace jtx;
151 IOU const iou{peer, currency};
152 Json::Value jv = trust(acc, iou(trustAmount_));
155 env(jv);
156 env.close();
157 };
158
159public:
161 std::uint32_t trustAmount = 100,
162 std::uint32_t initialBalance = 50)
163 // Use a deterministic seed so the unit tests run in a reproducible way
164 : engine_{1977u}
165 , trustAmount_{trustAmount}
166 , initialBalance_{initialBalance} {};
167
168 void
170 {
171 if (shouldSet())
172 env(rate(acc, transferRateDist_(engine_)));
173 }
174
175 // Set the initial balance, taking into account the qualities
176 void
178 jtx::Env& env,
179 jtx::Account const& acc,
180 jtx::Account const& peer,
181 Currency const& currency)
182 {
183 using namespace jtx;
184 IOU const iou{acc, currency};
185 // This payment sets the acc's balance to `initialBalance`.
186 // Since input qualities complicate this payment, use `sendMax` with
187 // `initialBalance` to make sure the balance is set correctly.
188 env(pay(peer, acc, iou(trustAmount_)),
191 env.close();
192 }
193
194 void
196 jtx::Env& env,
197 jtx::Account const& acc,
198 jtx::Account const& peer,
199 Currency const& currency)
200 {
201 using namespace jtx;
203 return;
204 setInitialBalance(env, acc, peer, currency);
205 }
206
207 // Setup the trust amounts and in/out qualities (but not the balances) on
208 // both sides of the trust line
209 void
211 jtx::Env& env,
212 jtx::Account const& acc1,
213 jtx::Account const& acc2,
214 Currency const& currency)
215 {
216 setupTrustLine(env, acc1, acc2, currency);
217 setupTrustLine(env, acc2, acc1, currency);
218 };
219};
220
222{
223 static std::string
224 prettyQuality(Quality const& q)
225 {
227 STAmount rate = q.rate();
228 sstr << rate << " (" << q << ")";
229 return sstr.str();
230 };
231
232 template <class Stream>
233 static void
234 logStrand(Stream& stream, Strand const& strand)
235 {
236 stream << "Strand:\n";
237 for (auto const& step : strand)
238 stream << "\n" << *step;
239 stream << "\n\n";
240 };
241
242 void
244 RippleCalcTestParams const& rcp,
246 std::optional<Quality> const& expectedQ = {})
247 {
248 PaymentSandbox sb(closed.get(), tapNONE);
249 AMMContext ammContext(rcp.srcAccount, false);
250
251 auto const sendMaxIssue = [&rcp]() -> std::optional<Issue> {
252 if (rcp.sendMax)
253 return rcp.sendMax->issue();
254 return std::nullopt;
255 }();
256
258
259 auto sr = toStrands(
260 sb,
261 rcp.srcAccount,
262 rcp.dstAccount,
263 rcp.dstAmt.issue(),
264 /*limitQuality*/ std::nullopt,
265 sendMaxIssue,
266 rcp.paths,
267 /*defaultPaths*/ rcp.paths.empty(),
268 sb.rules().enabled(featureOwnerPaysFee),
270 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, supported_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, supported_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:147
bool isMember(const char *key) const
Return true if the object has a member named key.
Definition: json_value.cpp:943
A generic endpoint for log messages.
Definition: Journal.h:59
static Sink & getNullSink()
Returns a Sink which does nothing.
A testsuite class.
Definition: suite.h:53
log_os< char > log
Logging output stream.
Definition: suite.h:150
testcase_t testcase
Memberspace for declaring test cases.
Definition: suite.h:153
std::string const & arg() const
Return the argument associated with the runner.
Definition: suite.h:286
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:487
bool empty() const
Definition: STPathSet.h:507
void emplace_back(Args &&... args)
Definition: STPathSet.h:416
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:38
A transaction testing environment.
Definition: Env.h:117
Json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition: Env.h:516
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:237
Converts to IOU Issue or STAmount.
Add a path.
Definition: paths.h:56
Set Paths, SendMax on a JTx.
Definition: paths.h:33
Sets the SendMax on a JTx.
Definition: sendmax.h:32
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:30
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:30
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:70
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: algorithm.h:26
constexpr Number one
Definition: Number.cpp:169
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:116
STAmount amountFromJson(SField const &name, Json::Value const &v)
Definition: STAmount.cpp:900
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:470
@ no
Definition: Steps.h:42
QualityDirection
Definition: Steps.h:40
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:242
@ 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:80
T str(T... args)
T to_string(T... args)
T value_or(T... args)