diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index bf457579825..8522baa7d81 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -755,6 +755,7 @@ else () src/test/app/SetRegularKey_test.cpp src/test/app/SetTrust_test.cpp src/test/app/Taker_test.cpp + src/test/app/TheoreticalQuality_test.cpp src/test/app/Ticket_test.cpp src/test/app/Transaction_ordering_test.cpp src/test/app/TrustAndBalance_test.cpp diff --git a/src/ripple/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index d6c88277712..3d737486693 100644 --- a/src/ripple/app/paths/impl/BookStep.cpp +++ b/src/ripple/app/paths/impl/BookStep.cpp @@ -123,8 +123,8 @@ class BookStep : public StepImp> return book_; } - boost::optional - qualityUpperBound(ReadView const& v, DebtDirection& dir) const override; + std::pair, DebtDirection> + qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const override; std::pair revImp ( @@ -247,7 +247,7 @@ class BookPaymentStep } Quality - qualityUpperBound(ReadView const& v, + adjustQualityWithFees(ReadView const& v, Quality const& ofrQ, DebtDirection prevStepDir) const { @@ -392,7 +392,7 @@ class BookOfferCrossingStep } Quality - qualityUpperBound(ReadView const& v, + adjustQualityWithFees(ReadView const& v, Quality const& ofrQ, DebtDirection prevStepDir) const { @@ -426,21 +426,22 @@ bool BookStep::equal (Step const& rhs) const } template -boost::optional +std::pair, DebtDirection> BookStep::qualityUpperBound( - ReadView const& v, DebtDirection& dir) const + ReadView const& v, + DebtDirection prevStepDir) const { - auto const prevStepDir = dir; - dir = this->debtDirection(v, StrandDirection::forward); + auto const dir = this->debtDirection(v, StrandDirection::forward); // This can be simplified (and sped up) if directories are never empty. Sandbox sb(&v, tapNONE); BookTip bt(sb, book_); if (!bt.step(j_)) - return boost::none; + return {boost::none, dir}; - return static_cast(this)->qualityUpperBound( + Quality const q = static_cast(this)->adjustQualityWithFees( v, bt.quality(), prevStepDir); + return {q, dir}; } // Adjust the offer amount and step amount subject to the given input limit diff --git a/src/ripple/app/paths/impl/DirectStep.cpp b/src/ripple/app/paths/impl/DirectStep.cpp index e9d84d5eac2..92d600c987f 100644 --- a/src/ripple/app/paths/impl/DirectStep.cpp +++ b/src/ripple/app/paths/impl/DirectStep.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -153,8 +154,8 @@ class DirectStepI : public StepImp> std::uint32_t lineQualityIn (ReadView const& v) const override; - boost::optional - qualityUpperBound(ReadView const& v, DebtDirection& dir) const override; + std::pair, DebtDirection> + qualityUpperBound(ReadView const& v, DebtDirection dir) const override; std::pair revImp ( @@ -820,24 +821,42 @@ DirectStepI::lineQualityIn (ReadView const& v) const } template -boost::optional -DirectStepI::qualityUpperBound(ReadView const& v, DebtDirection& dir) +std::pair, DebtDirection> +DirectStepI::qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const { - auto const prevStepDebtDir = dir; - dir = this->debtDirection(v, StrandDirection::forward); - std::uint32_t const srcQOut = [&]() -> std::uint32_t { - if (redeems(prevStepDebtDir) && issues(dir)) - return transferRate(v, src_).value; - return QUALITY_ONE; - }(); - auto dstQIn = - static_cast(this)->quality(v, QualityDirection::in); + auto const dir = this->debtDirection(v, StrandDirection::forward); + + if (!v.rules().enabled(fixQualityUpperBound)) + { + std::uint32_t const srcQOut = [&]() -> std::uint32_t { + if (redeems(prevStepDir) && issues(dir)) + return transferRate(v, src_).value; + return QUALITY_ONE; + }(); + auto dstQIn = static_cast(this)->quality( + v, QualityDirection::in); + + if (isLast_ && dstQIn > QUALITY_ONE) + dstQIn = QUALITY_ONE; + Issue const iss{currency_, src_}; + return {Quality(getRate(STAmount(iss, srcQOut), STAmount(iss, dstQIn))), + dir}; + } + + auto const [srcQOut, dstQIn] = redeems(dir) + ? qualitiesSrcRedeems(v) + : qualitiesSrcIssues(v, prevStepDir); - if (isLast_ && dstQIn > QUALITY_ONE) - dstQIn = QUALITY_ONE; Issue const iss{currency_, src_}; - return Quality(getRate(STAmount(iss, srcQOut), STAmount(iss, dstQIn))); + // Be careful not to switch the parameters to `getRate`. The + // `getRate(offerOut, offerIn)` function is usually used for offers. It + // returns offerIn/offerOut. For a direct step, the rate is srcQOut/dstQIn + // (Input*dstQIn/srcQOut = Output; So rate = srcQOut/dstQIn). Although the + // first parameter is called `offerOut`, it should take the `dstQIn` + // variable. + return {Quality(getRate(STAmount(iss, dstQIn), STAmount(iss, srcQOut))), + dir}; } template diff --git a/src/ripple/app/paths/impl/Steps.h b/src/ripple/app/paths/impl/Steps.h index c147725c1ec..55bbcc5a41c 100644 --- a/src/ripple/app/paths/impl/Steps.h +++ b/src/ripple/app/paths/impl/Steps.h @@ -176,18 +176,24 @@ class Step return QUALITY_ONE; } + // clang-format off /** Find an upper bound of quality for the step @param v view to query the ledger state from - @param dir in/out param. Set to DebtDirection::redeems if the previous step redeems. - Will be set to DebtDirection::redeems if this step redeems; Will be set to DebtDirection::issues if this - step does not redeem - @return The upper bound of quality for the step, or boost::none if the - step is dry. + @param prevStepDir Set to DebtDirection::redeems if the previous step redeems. + @return A pair. The first element is the upper bound of quality for the step, or boost::none if the + step is dry. The second element will be set to DebtDirection::redeems if this steps redeems, + DebtDirection:issues if this step issues. + @note it is an upper bound because offers on the books may be unfunded. + If there is always a funded offer at the tip of the book, then we could + rename this `theoreticalQuality` rather than `qualityUpperBound`. It + could still differ from the actual quality, but except for "dust" amounts, + it should be a good estimate for the actual quality. */ - virtual boost::optional - qualityUpperBound(ReadView const& v, DebtDirection& dir) const = 0; + // clang-format on + virtual std::pair, DebtDirection> + qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const = 0; /** If this step is a BookStep, return the book. diff --git a/src/ripple/app/paths/impl/StrandFlow.h b/src/ripple/app/paths/impl/StrandFlow.h index 62192b8e99a..7e217eb78d5 100644 --- a/src/ripple/app/paths/impl/StrandFlow.h +++ b/src/ripple/app/paths/impl/StrandFlow.h @@ -320,6 +320,24 @@ struct FlowResult }; /// @endcond +/// @cond INTERNAL +inline boost::optional +qualityUpperBound(ReadView const& v, Strand const& strand) +{ + Quality q{STAmount::uRateOne}; + boost::optional stepQ; + DebtDirection dir = DebtDirection::issues; + for (auto const& step : strand) + { + if (std::tie(stepQ, dir) = step->qualityUpperBound(v, dir); stepQ) + q = composed_quality(q, *stepQ); + else + return boost::none; + } + return q; +}; +/// @endcond + /// @cond INTERNAL /* Track the non-dry strands @@ -396,23 +414,6 @@ class ActiveStrands }; /// @endcond -/// @cond INTERNAL -boost::optional -qualityUpperBound(ReadView const& v, Strand const& strand) -{ - Quality q{STAmount::uRateOne}; - DebtDirection dir = DebtDirection::issues; - for (auto const& step : strand) - { - if (auto const stepQ = step->qualityUpperBound(v, dir)) - q = composed_quality(q, *stepQ); - else - return boost::none; - } - return q; -}; -/// @endcond - /** Request `out` amount from a collection of strands diff --git a/src/ripple/app/paths/impl/XRPEndpointStep.cpp b/src/ripple/app/paths/impl/XRPEndpointStep.cpp index e5d4f5e5293..2c6b91680fe 100644 --- a/src/ripple/app/paths/impl/XRPEndpointStep.cpp +++ b/src/ripple/app/paths/impl/XRPEndpointStep.cpp @@ -95,8 +95,8 @@ class XRPEndpointStep : public StepImp< return DebtDirection::issues; } - boost::optional - qualityUpperBound(ReadView const& v, DebtDirection& dir) const override; + std::pair, DebtDirection> + qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const override; std::pair revImp ( @@ -241,15 +241,14 @@ inline bool operator==(XRPEndpointStep const& lhs, } template -boost::optional +std::pair, DebtDirection> XRPEndpointStep::qualityUpperBound( - ReadView const& v, DebtDirection& dir) const + ReadView const& v, DebtDirection prevStepDir) const { - dir = this->debtDirection(v, StrandDirection::forward); - return Quality{STAmount::uRateOne}; + return {Quality{STAmount::uRateOne}, + this->debtDirection(v, StrandDirection::forward)}; } - template std::pair XRPEndpointStep::revImp ( diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index e3b70d82d47..ab850f72208 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -108,6 +108,8 @@ class FeatureCollections "fixCheckThreading", "fixPayChanRecipientOwnerDir", "DeletableAccounts", + // fixQualityUpperBound should be activated before FlowCross + "fixQualityUpperBound", }; std::vector features; @@ -394,6 +396,7 @@ extern uint256 const fixMasterKeyAsRegularKey; extern uint256 const fixCheckThreading; extern uint256 const fixPayChanRecipientOwnerDir; extern uint256 const featureDeletableAccounts; +extern uint256 const fixQualityUpperBound; } // ripple diff --git a/src/ripple/protocol/Quality.h b/src/ripple/protocol/Quality.h index e2048ed3c2b..36b7275e62a 100644 --- a/src/ripple/protocol/Quality.h +++ b/src/ripple/protocol/Quality.h @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -122,6 +123,9 @@ class Quality static const int maxTickSize = 16; private: + // This has the same representation as STAmount, see the comment on the STAmount. + // However, this class does not alway use the canonical representation. In particular, + // the increment and decrement operators may cause a non-canonical representation. value_type m_value; public: @@ -270,6 +274,39 @@ class Quality os << quality.m_value; return os; } + + // return the relative distance (relative error) between two qualities. This is used for testing only. + // relative distance is abs(a-b)/min(a,b) + friend double + relativeDistance(Quality const& q1, Quality const& q2) + { + assert(q1.m_value > 0 && q2.m_value > 0); + + if (q1.m_value == q2.m_value) // make expected common case fast + return 0; + + auto const [minV, maxV] = std::minmax(q1.m_value, q2.m_value); + + auto mantissa = [](std::uint64_t rate) { + return rate & ~(255ull << (64 - 8)); + }; + auto exponent = [](std::uint64_t rate) { + return static_cast(rate >> (64 - 8)) - 100; + }; + + auto const minVMantissa = mantissa(minV); + auto const maxVMantissa = mantissa(maxV); + auto const expDiff = exponent(maxV) - exponent(minV); + + double const minVD = static_cast(minVMantissa); + double const maxVD = expDiff ? maxVMantissa * pow(10, expDiff) + : static_cast(maxVMantissa); + + // maxVD and minVD are scaled so they have the same exponents. Dividing + // cancels out the exponents, so we only need to deal with the (scaled) + // mantissas + return (maxVD - minVD) / minVD; + } }; /** Calculate the quality of a two-hop path given the two hops. diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 1cf8bb57207..1e5d30f9784 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -128,6 +128,7 @@ detail::supportedAmendments () "fixCheckThreading", "fixPayChanRecipientOwnerDir", "DeletableAccounts", + "fixQualityUpperBound", }; return supported; } @@ -185,5 +186,6 @@ uint256 const fixMasterKeyAsRegularKey = *getRegisteredFeature("fixMasterKeyAsRe uint256 const fixCheckThreading = *getRegisteredFeature("fixCheckThreading"); uint256 const fixPayChanRecipientOwnerDir = *getRegisteredFeature("fixPayChanRecipientOwnerDir"); uint256 const featureDeletableAccounts = *getRegisteredFeature("DeletableAccounts"); +uint256 const fixQualityUpperBound = *getRegisteredFeature("fixQualityUpperBound"); } // ripple diff --git a/src/test/app/TheoreticalQuality_test.cpp b/src/test/app/TheoreticalQuality_test.cpp new file mode 100644 index 00000000000..b0dbb2c4142 --- /dev/null +++ b/src/test/app/TheoreticalQuality_test.cpp @@ -0,0 +1,555 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +struct RippleCalcTestParams +{ + AccountID srcAccount; + AccountID dstAccount; + + STAmount dstAmt; + boost::optional sendMax; + + STPathSet paths; + + explicit RippleCalcTestParams(Json::Value const& jv) + : srcAccount{*parseBase58(jv[jss::Account].asString())} + , dstAccount{*parseBase58(jv[jss::Destination].asString())} + , dstAmt{amountFromJson(sfAmount, jv[jss::Amount])} + { + if (jv.isMember(jss::SendMax)) + sendMax = amountFromJson(sfSendMax, jv[jss::SendMax]); + + if (jv.isMember(jss::Paths)) + { + // paths is an array of arrays + // each leaf element will be of the form + for (auto const& path : jv[jss::Paths]) + { + STPath p; + for (auto const& pe : path) + { + if (pe.isMember(jss::account)) + { + assert( + !pe.isMember(jss::currency) && + !pe.isMember(jss::issuer)); + p.emplace_back( + *parseBase58( + pe[jss::account].asString()), + boost::none, + boost::none); + } + else if ( + pe.isMember(jss::currency) && pe.isMember(jss::issuer)) + { + auto const currency = + to_currency(pe[jss::currency].asString()); + boost::optional issuer; + if (!isXRP(currency)) + issuer = *parseBase58( + pe[jss::issuer].asString()); + else + assert(isXRP(*parseBase58( + pe[jss::issuer].asString()))); + p.emplace_back(boost::none, currency, issuer); + } + else + { + assert(0); + } + } + paths.emplace_back(std::move(p)); + } + } + } +}; + +// Class to randomly set an account's transfer rate, quality in, quality out, +// and initial balance +class RandomAccountParams +{ + beast::xor_shift_engine engine_; + std::uint32_t const trustAmount_; + // Balance to set if an account redeems into another account. Otherwise + // the balance will be zero. Since we are testing quality measures, the + // payment should not use multiple qualities, so the initialBalance + // needs to be able to handle an entire payment (otherwise an account + // will go from redeeming to issuing and the fees/qualities can change) + std::uint32_t const initialBalance_; + + // probability of changing a value from its default + constexpr static double probChangeDefault_ = 0.75; + // probability that an account redeems into another account + constexpr static double probRedeem_ = 0.5; + std::uniform_real_distribution<> zeroOneDist_{0.0, 1.0}; + std::uniform_real_distribution<> transferRateDist_{1.0, 2.0}; + std::uniform_real_distribution<> qualityPercentDist_{80, 120}; + + bool + shouldSet() + { + return zeroOneDist_(engine_) <= probChangeDefault_; + }; + + void + maybeInsertQuality(Json::Value& jv, QualityDirection qDir) + { + if (!shouldSet()) + return; + + auto const percent = qualityPercentDist_(engine_); + auto const& field = + qDir == QualityDirection::in ? sfQualityIn : sfQualityOut; + auto const value = + static_cast((percent / 100) * QUALITY_ONE); + jv[field.jsonName] = value; + }; + + // Setup the trust amounts and in/out qualities (but not the balances) + void + setupTrustLine( + jtx::Env& env, + jtx::Account const& acc, + jtx::Account const& peer, + Currency const& currency) + { + using namespace jtx; + IOU const iou{peer, currency}; + Json::Value jv = trust(acc, iou(trustAmount_)); + maybeInsertQuality(jv, QualityDirection::in); + maybeInsertQuality(jv, QualityDirection::out); + env(jv); + env.close(); + }; + +public: + explicit RandomAccountParams( + std::uint32_t trustAmount = 100, + std::uint32_t initialBalance = 50) + // Use a deterministic seed so the unit tests run in a reproducible way + : engine_{1977u} + , trustAmount_{trustAmount} + , initialBalance_{initialBalance} {}; + + void + maybeSetTransferRate(jtx::Env& env, jtx::Account const& acc) + { + if (shouldSet()) + env(rate(acc, transferRateDist_(engine_))); + } + + // Set the initial balance, taking into account the qualities + void + setInitialBalance( + jtx::Env& env, + jtx::Account const& acc, + jtx::Account const& peer, + Currency const& currency) + { + using namespace jtx; + IOU const iou{acc, currency}; + // This payment sets the acc's balance to `initialBalance`. + // Since input qualities complicate this payment, use `sendMax` with + // `initialBalance` to make sure the balance is set correctly. + env(pay(peer, acc, iou(trustAmount_)), + sendmax(iou(initialBalance_)), + txflags(tfPartialPayment)); + env.close(); + } + + void + maybeSetInitialBalance( + jtx::Env& env, + jtx::Account const& acc, + jtx::Account const& peer, + Currency const& currency) + { + using namespace jtx; + if (zeroOneDist_(engine_) > probRedeem_) + return; + setInitialBalance(env, acc, peer, currency); + } + + // Setup the trust amounts and in/out qualities (but not the balances) on + // both sides of the trust line + void + setupTrustLines( + jtx::Env& env, + jtx::Account const& acc1, + jtx::Account const& acc2, + Currency const& currency) + { + setupTrustLine(env, acc1, acc2, currency); + setupTrustLine(env, acc2, acc1, currency); + }; +}; + +class TheoreticalQuality_test : public beast::unit_test::suite +{ + static std::string + prettyQuality(Quality const& q) + { + std::stringstream sstr; + STAmount rate = q.rate(); + sstr << rate << " (" << q << ")"; + return sstr.str(); + }; + + template + static void + logStrand(Stream& stream, Strand const& strand) + { + stream << "Strand:\n"; + for (auto const& step : strand) + stream << "\n" << *step; + stream << "\n\n"; + }; + + void + testCase( + RippleCalcTestParams const& rcp, + std::shared_ptr closed, + boost::optional const& expectedQ = {}) + { + PaymentSandbox sb(closed.get(), tapNONE); + + auto const sendMaxIssue = [&rcp]() -> boost::optional { + if (rcp.sendMax) + return rcp.sendMax->issue(); + return boost::none; + }(); + + beast::Journal dummyJ{beast::Journal::getNullSink()}; + + auto sr = toStrands( + sb, + rcp.srcAccount, + rcp.dstAccount, + rcp.dstAmt.issue(), + /*limitQuality*/ boost::none, + sendMaxIssue, + rcp.paths, + /*defaultPaths*/ rcp.paths.empty(), + sb.rules().enabled(featureOwnerPaysFee), + /*offerCrossing*/ false, + dummyJ); + + BEAST_EXPECT(sr.first == tesSUCCESS); + + if (sr.first != tesSUCCESS) + return; + + // Due to the floating point calculations, theoretical and actual + // qualities are not expected to always be exactly equal. However, they + // should always be very close. This function checks that that two + // qualities are "close enough". + auto compareClose = [](Quality const& q1, Quality const& q2) { + // relative diff is fabs(a-b)/min(a,b) + // can't get access to internal value. Use the rate + constexpr double tolerance = 0.0000001; + return relativeDistance(q1, q2) <= tolerance; + }; + + for (auto const& strand : sr.second) + { + Quality const theoreticalQ = *qualityUpperBound(sb, strand); + auto const f = flow( + sb, strand, IOUAmount(10, 0), IOUAmount(5, 0), dummyJ); + BEAST_EXPECT(f.success); + Quality const actualQ(f.out, f.in); + if (actualQ != theoreticalQ && !compareClose(actualQ, theoreticalQ)) + { + BEAST_EXPECT(actualQ == theoreticalQ); // get the failure + log << "\nAcutal != Theoretical\n"; + log << "\nTQ: " << prettyQuality(theoreticalQ) << "\n"; + log << "AQ: " << prettyQuality(actualQ) << "\n"; + logStrand(log, strand); + } + if (expectedQ && expectedQ != theoreticalQ && + !compareClose(*expectedQ, theoreticalQ)) + { + BEAST_EXPECT(expectedQ == theoreticalQ); // get the failure + log << "\nExpected != Theoretical\n"; + log << "\nTQ: " << prettyQuality(theoreticalQ) << "\n"; + log << "EQ: " << prettyQuality(*expectedQ) << "\n"; + logStrand(log, strand); + } + }; + } + +public: + void + testDirectStep(boost::optional const& reqNumIterations) + { + testcase("Direct Step"); + + // clang-format off + + // Set up a payment through four accounts: alice -> bob -> carol -> dan + // For each relevant trust line on the path, there are three things that can vary: + // 1) input quality + // 2) output quality + // 3) debt direction + // For each account, there is one thing that can vary: + // 1) transfer rate + + // clang-format on + + using namespace jtx; + + auto const currency = to_currency("USD"); + + constexpr std::size_t const numAccounts = 4; + + // There are three relevant trust lines: `alice->bob`, `bob->carol`, and + // `carol->dan`. There are four accounts. If we count the number of + // combinations of parameters where a parameter is changed from its + // default value, there are + // 2^(num_trust_lines*num_trust_qualities+numAccounts) combinations of + // values to test, or 2^13 combinations. Use this value to set the + // number of iterations. Note however that many of these parameter + // combinations run essentially the same test. For example, changing the + // quality values for bob and carol test almost the same thing. + // Similarly, changing the transfer rates on bob and carol test almost + // the same thing. Instead of systematically running these 8k tests, + // randomly sample the test space. + int const numTestIterations = reqNumIterations.value_or(250); + + constexpr std::uint32_t paymentAmount = 1; + + // Class to randomly set account transfer rates, qualities, and other + // params. + RandomAccountParams rndAccParams; + + // Tests are sped up by a factor of 2 if a new environment isn't created + // on every iteration. + Env env(*this, supported_amendments()); + for (int i = 0; i < numTestIterations; ++i) + { + auto const iterAsStr = std::to_string(i); + // New set of accounts on every iteration so the environment doesn't + // need to be recreated (2x speedup) + auto const alice = Account("alice" + iterAsStr); + auto const bob = Account("bob" + iterAsStr); + auto const carol = Account("carol" + iterAsStr); + auto const dan = Account("dan" + iterAsStr); + std::array accounts{{alice, bob, carol, dan}}; + static_assert( + numAccounts == 4, "Path is only correct for four accounts"); + path const accountsPath(accounts[1], accounts[2]); + env.fund(XRP(10000), alice, bob, carol, dan); + env.close(); + + // iterate through all pairs of accounts, randomly set the transfer + // rate, qIn, qOut, and if the account issues or redeems + for (std::size_t ii = 0; ii < numAccounts; ++ii) + { + rndAccParams.maybeSetTransferRate(env, accounts[ii]); + // The payment is from: + // account[0] -> account[1] -> account[2] -> account[3] + // set the trust lines and initial balances for each pair of + // neighboring accounts + std::size_t const j = ii + 1; + if (j == numAccounts) + continue; + + rndAccParams.setupTrustLines( + env, accounts[ii], accounts[j], currency); + rndAccParams.maybeSetInitialBalance( + env, accounts[ii], accounts[j], currency); + } + + // Accounts are set up, make the payment + IOU const iou{accounts.back(), currency}; + RippleCalcTestParams rcp{env.json( + pay(accounts.front(), accounts.back(), iou(paymentAmount)), + accountsPath, + txflags(tfNoRippleDirect))}; + + testCase(rcp, env.closed()); + } + } + + void + testBookStep(boost::optional const& reqNumIterations) + { + testcase("Book Step"); + using namespace jtx; + + // clang-format off + + // Setup a payment through an offer: alice (USD/bob) -> bob -> (USD/bob)|(EUR/carol) -> carol -> dan + // For each relevant trust line, vary input quality, output quality, debt direction. + // For each account, vary transfer rate. + // The USD/bob|EUR/carol offer owner is "Oscar" + + // clang-format on + + int const numTestIterations = reqNumIterations.value_or(100); + + constexpr std::uint32_t paymentAmount = 1; + + Currency const eurCurrency = to_currency("EUR"); + Currency const usdCurrency = to_currency("USD"); + + // Class to randomly set account transfer rates, qualities, and other + // params. + RandomAccountParams rndAccParams; + + // Speed up tests by creating the environment outside the loop + // (factor of 2 speedup on the DirectStep tests) + Env env(*this, supported_amendments()); + for (int i = 0; i < numTestIterations; ++i) + { + auto const iterAsStr = std::to_string(i); + auto const alice = Account("alice" + iterAsStr); + auto const bob = Account("bob" + iterAsStr); + auto const carol = Account("carol" + iterAsStr); + auto const dan = Account("dan" + iterAsStr); + auto const oscar = Account("oscar" + iterAsStr); // offer owner + auto const USDB = bob["USD"]; + auto const EURC = carol["EUR"]; + constexpr std::size_t const numAccounts = 5; + std::array accounts{ + {alice, bob, carol, dan, oscar}}; + + // sendmax should be in USDB and delivered amount should be in EURC + // normalized path should be: + // alice -> bob -> (USD/bob)|(EUR/carol) -> carol -> dan + path const bookPath(~EURC); + + env.fund(XRP(10000), alice, bob, carol, dan, oscar); + env.close(); + + for (auto const& acc : accounts) + rndAccParams.maybeSetTransferRate(env, acc); + + for (auto const& currency : {usdCurrency, eurCurrency}) + { + rndAccParams.setupTrustLines( + env, alice, bob, currency); // first step in payment + rndAccParams.setupTrustLines( + env, carol, dan, currency); // last step in payment + rndAccParams.setupTrustLines( + env, oscar, bob, currency); // offer owner + rndAccParams.setupTrustLines( + env, oscar, carol, currency); // offer owner + } + + rndAccParams.maybeSetInitialBalance(env, alice, bob, usdCurrency); + rndAccParams.maybeSetInitialBalance(env, carol, dan, eurCurrency); + rndAccParams.setInitialBalance(env, oscar, bob, usdCurrency); + rndAccParams.setInitialBalance(env, oscar, carol, eurCurrency); + + env(offer(oscar, USDB(50), EURC(50))); + env.close(); + + // Accounts are set up, make the payment + IOU const srcIOU{bob, usdCurrency}; + IOU const dstIOU{carol, eurCurrency}; + RippleCalcTestParams rcp{env.json( + pay(alice, dan, dstIOU(paymentAmount)), + sendmax(srcIOU(100 * paymentAmount)), + bookPath, + txflags(tfNoRippleDirect))}; + + testCase(rcp, env.closed()); + } + } + + void + testRelativeQDistance() + { + testcase("Relative quality distance"); + + auto toQuality = [](std::uint64_t mantissa, + int exponent = 0) -> Quality { + // The only way to construct a Quality from an STAmount is to take + // their ratio. Set the denominator STAmount to `one` to easily + // create a quality from a single amount + STAmount const one{noIssue(), 1}; + STAmount const v{noIssue(), mantissa, exponent}; + return Quality{one, v}; + }; + + BEAST_EXPECT(relativeDistance(toQuality(100), toQuality(100)) == 0); + BEAST_EXPECT(relativeDistance(toQuality(100), toQuality(100, 1)) == 9); + BEAST_EXPECT(relativeDistance(toQuality(100), toQuality(110)) == .1); + BEAST_EXPECT( + relativeDistance(toQuality(100, 90), toQuality(110, 90)) == .1); + BEAST_EXPECT( + relativeDistance(toQuality(100, 90), toQuality(110, 91)) == 10); + BEAST_EXPECT( + relativeDistance(toQuality(100, 0), toQuality(100, 90)) == 1e90); + // Make the mantissa in the smaller value bigger than the mantissa in + // the larger value. Instead of checking the exact result, we check that + // it's large. If the values did not compare correctly in + // `relativeDistance`, then the returned value would be negative. + BEAST_EXPECT( + relativeDistance(toQuality(102, 0), toQuality(101, 90)) >= 1e89); + } + + void + run() override + { + // Use the command line argument `--unittest-arg=500 ` to change the + // number of iterations to 500 + auto const numIterations = [s = arg()]() -> boost::optional { + if (s.empty()) + return boost::none; + try + { + std::size_t pos; + auto const r = stoi(s, &pos); + if (pos != s.size()) + return boost::none; + return r; + } + catch (...) + { + return boost::none; + } + }(); + testRelativeQDistance(); + testDirectStep(numIterations); + testBookStep(numIterations); + } +}; + +BEAST_DEFINE_TESTSUITE(TheoreticalQuality, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/unity/app_test_unity2.cpp b/src/test/unity/app_test_unity2.cpp index 1f4b9678fc6..46f7ce10e55 100644 --- a/src/test/unity/app_test_unity2.cpp +++ b/src/test/unity/app_test_unity2.cpp @@ -25,11 +25,12 @@ #include #include #include +#include #include #include #include -#include #include +#include #include #include #include