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