mirror of
https://github.com/Xahau/xahaud.git
synced 2025-12-06 17:27:52 +00:00
* 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
556 lines
20 KiB
C++
556 lines
20 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
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
|