mirror of
https://github.com/Xahau/xahaud.git
synced 2025-12-06 17:27:52 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
555
src/test/app/TheoreticalQuality_test.cpp
Normal file
555
src/test/app/TheoreticalQuality_test.cpp
Normal 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
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user