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