Fix bug in qualityUpperBound:

* In and Out parameters were swapped when calculating the rate
* In and out qualities were not calculated correctly; use existing functions
  to get the qualities
* Added tests to check that theoretical quality matches actual computed quality
* Remove in/out parameter from qualityUpperBound
* Rename an overload of qualityUpperBound to adjustQualityWithFees
* Add fix amendment
This commit is contained in:
seelabs
2020-01-27 10:25:27 -08:00
committed by Manoj doshi
parent ae707b814f
commit 9d3626fec5
11 changed files with 683 additions and 58 deletions

View File

@@ -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

View File

@@ -123,8 +123,8 @@ public:
return book_;
}
boost::optional<Quality>
qualityUpperBound(ReadView const& v, DebtDirection& dir) const override;
std::pair<boost::optional<Quality>, DebtDirection>
qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const override;
std::pair<TIn, TOut>
revImp (
@@ -247,7 +247,7 @@ public:
}
Quality
qualityUpperBound(ReadView const& v,
adjustQualityWithFees(ReadView const& v,
Quality const& ofrQ,
DebtDirection prevStepDir) const
{
@@ -392,7 +392,7 @@ public:
}
Quality
qualityUpperBound(ReadView const& v,
adjustQualityWithFees(ReadView const& v,
Quality const& ofrQ,
DebtDirection prevStepDir) const
{
@@ -426,21 +426,22 @@ bool BookStep<TIn, TOut, TDerived>::equal (Step const& rhs) const
}
template <class TIn, class TOut, class TDerived>
boost::optional<Quality>
std::pair<boost::optional<Quality>, DebtDirection>
BookStep<TIn, TOut, TDerived>::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<TDerived const*>(this)->qualityUpperBound(
Quality const q = static_cast<TDerived const*>(this)->adjustQualityWithFees(
v, bt.quality(), prevStepDir);
return {q, dir};
}
// Adjust the offer amount and step amount subject to the given input limit

View File

@@ -23,6 +23,7 @@
#include <ripple/basics/IOUAmount.h>
#include <ripple/basics/Log.h>
#include <ripple/ledger/PaymentSandbox.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Quality.h>
#include <boost/container/flat_set.hpp>
@@ -153,8 +154,8 @@ public:
std::uint32_t
lineQualityIn (ReadView const& v) const override;
boost::optional<Quality>
qualityUpperBound(ReadView const& v, DebtDirection& dir) const override;
std::pair<boost::optional<Quality>, DebtDirection>
qualityUpperBound(ReadView const& v, DebtDirection dir) const override;
std::pair<IOUAmount, IOUAmount>
revImp (
@@ -820,24 +821,42 @@ DirectStepI<TDerived>::lineQualityIn (ReadView const& v) const
}
template <class TDerived>
boost::optional<Quality>
DirectStepI<TDerived>::qualityUpperBound(ReadView const& v, DebtDirection& dir)
std::pair<boost::optional<Quality>, DebtDirection>
DirectStepI<TDerived>::qualityUpperBound(ReadView const& v, DebtDirection prevStepDir)
const
{
auto const prevStepDebtDir = dir;
dir = this->debtDirection(v, StrandDirection::forward);
auto const dir = this->debtDirection(v, StrandDirection::forward);
if (!v.rules().enabled(fixQualityUpperBound))
{
std::uint32_t const srcQOut = [&]() -> std::uint32_t {
if (redeems(prevStepDebtDir) && issues(dir))
if (redeems(prevStepDir) && issues(dir))
return transferRate(v, src_).value;
return QUALITY_ONE;
}();
auto dstQIn =
static_cast<TDerived const*>(this)->quality(v, QualityDirection::in);
auto dstQIn = static_cast<TDerived const*>(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)));
return {Quality(getRate(STAmount(iss, srcQOut), STAmount(iss, dstQIn))),
dir};
}
auto const [srcQOut, dstQIn] = redeems(dir)
? qualitiesSrcRedeems(v)
: qualitiesSrcIssues(v, prevStepDir);
Issue const iss{currency_, src_};
// 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 <class TDerived>

View File

@@ -176,18 +176,24 @@ public:
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<Quality>
qualityUpperBound(ReadView const& v, DebtDirection& dir) const = 0;
// clang-format on
virtual std::pair<boost::optional<Quality>, DebtDirection>
qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const = 0;
/**
If this step is a BookStep, return the book.

View File

@@ -320,6 +320,24 @@ struct FlowResult
};
/// @endcond
/// @cond INTERNAL
inline boost::optional<Quality>
qualityUpperBound(ReadView const& v, Strand const& strand)
{
Quality q{STAmount::uRateOne};
boost::optional<Quality> 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 @@ public:
};
/// @endcond
/// @cond INTERNAL
boost::optional<Quality>
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

View File

@@ -95,8 +95,8 @@ public:
return DebtDirection::issues;
}
boost::optional<Quality>
qualityUpperBound(ReadView const& v, DebtDirection& dir) const override;
std::pair<boost::optional<Quality>, DebtDirection>
qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const override;
std::pair<XRPAmount, XRPAmount>
revImp (
@@ -241,15 +241,14 @@ inline bool operator==(XRPEndpointStep<TDerived> const& lhs,
}
template <class TDerived>
boost::optional<Quality>
std::pair<boost::optional<Quality>, DebtDirection>
XRPEndpointStep<TDerived>::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 <class TDerived>
std::pair<XRPAmount, XRPAmount>
XRPEndpointStep<TDerived>::revImp (

View File

@@ -108,6 +108,8 @@ class FeatureCollections
"fixCheckThreading",
"fixPayChanRecipientOwnerDir",
"DeletableAccounts",
// fixQualityUpperBound should be activated before FlowCross
"fixQualityUpperBound",
};
std::vector<uint256> 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

View File

@@ -25,6 +25,7 @@
#include <ripple/protocol/AmountConversions.h>
#include <ripple/protocol/STAmount.h>
#include <algorithm>
#include <cstdint>
#include <ostream>
@@ -122,6 +123,9 @@ public:
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 @@ public:
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<int>(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<double>(minVMantissa);
double const maxVD = expDiff ? maxVMantissa * pow(10, expDiff)
: static_cast<double>(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.

View File

@@ -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

View File

@@ -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 <ripple/app/paths/Flow.h>
#include <ripple/app/paths/impl/Steps.h>
#include <ripple/app/paths/impl/StrandFlow.h>
#include <ripple/basics/contract.h>
#include <ripple/basics/random.h>
#include <ripple/core/Config.h>
#include <ripple/ledger/ApplyViewImpl.h>
#include <ripple/ledger/PaymentSandbox.h>
#include <ripple/ledger/Sandbox.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
#include <test/jtx/PathSet.h>
namespace ripple {
namespace test {
struct RippleCalcTestParams
{
AccountID srcAccount;
AccountID dstAccount;
STAmount dstAmt;
boost::optional<STAmount> sendMax;
STPathSet paths;
explicit RippleCalcTestParams(Json::Value const& jv)
: srcAccount{*parseBase58<AccountID>(jv[jss::Account].asString())}
, dstAccount{*parseBase58<AccountID>(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<AccountID>(
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<AccountID> issuer;
if (!isXRP(currency))
issuer = *parseBase58<AccountID>(
pe[jss::issuer].asString());
else
assert(isXRP(*parseBase58<AccountID>(
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<std::uint32_t>((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 <class Stream>
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<ReadView const> closed,
boost::optional<Quality> const& expectedQ = {})
{
PaymentSandbox sb(closed.get(), tapNONE);
auto const sendMaxIssue = [&rcp]() -> boost::optional<Issue> {
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<IOUAmount, IOUAmount>(
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<int> 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<Account, numAccounts> 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<int> 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<Account, numAccounts> 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<int> {
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

View File

@@ -25,11 +25,12 @@
#include <test/app/RCLCensorshipDetector_test.cpp>
#include <test/app/RCLValidations_test.cpp>
#include <test/app/Regression_test.cpp>
#include <test/app/SHAMapStore_test.cpp>
#include <test/app/SetAuth_test.cpp>
#include <test/app/SetRegularKey_test.cpp>
#include <test/app/SetTrust_test.cpp>
#include <test/app/SHAMapStore_test.cpp>
#include <test/app/Taker_test.cpp>
#include <test/app/TheoreticalQuality_test.cpp>
#include <test/app/Ticket_test.cpp>
#include <test/app/Transaction_ordering_test.cpp>
#include <test/app/TrustAndBalance_test.cpp>