diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 4bad3a87b4a..dca4720e381 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -50,6 +50,7 @@ target_sources (xrpl_core PRIVATE src/ripple/basics/impl/FileUtilities.cpp src/ripple/basics/impl/IOUAmount.cpp src/ripple/basics/impl/Log.cpp + src/ripple/basics/impl/Number.cpp src/ripple/basics/impl/StringUtilities.cpp #[===============================[ main sources: @@ -153,6 +154,7 @@ install ( src/ripple/basics/LocalValue.h src/ripple/basics/Log.h src/ripple/basics/MathUtilities.h + src/ripple/basics/Number.h src/ripple/basics/safe_cast.h src/ripple/basics/Slice.h src/ripple/basics/spinlock.h @@ -737,6 +739,7 @@ if (tests) src/test/basics/FileUtilities_test.cpp src/test/basics/IOUAmount_test.cpp src/test/basics/KeyCache_test.cpp + src/test/basics/Number_test.cpp src/test/basics/PerfLog_test.cpp src/test/basics/RangeSet_test.cpp src/test/basics/scope_test.cpp diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index 8424b1d29af..bf278970bb8 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -293,6 +293,7 @@ TxQ::MaybeTx::apply(Application& app, OpenView& view, beast::Journal j) // If the rules or flags change, preflight again assert(pfresult); STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; + NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; if (pfresult->rules != view.rules() || pfresult->flags != flags) { @@ -717,6 +718,7 @@ TxQ::apply( beast::Journal j) { STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; + NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; // See if the transaction paid a high enough fee that it can go straight // into the ledger. diff --git a/src/ripple/app/paths/impl/AmountSpec.h b/src/ripple/app/paths/impl/AmountSpec.h index 927c3d72fe5..ca814c7b3ac 100644 --- a/src/ripple/app/paths/impl/AmountSpec.h +++ b/src/ripple/app/paths/impl/AmountSpec.h @@ -36,7 +36,7 @@ struct AmountSpec union { XRPAmount xrp; - IOUAmount iou; + IOUAmount iou = {}; }; std::optional issuer; std::optional currency; @@ -64,7 +64,7 @@ struct EitherAmount union { - IOUAmount iou; + IOUAmount iou = {}; XRPAmount xrp; }; diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 4c1a7e726cd..1eecccedb25 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -782,6 +782,7 @@ Transactor::operator()() JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID(); STAmountSO stAmountSO{view().rules().enabled(fixSTAmountCanonicalize)}; + NumberSO stNumberSO{view().rules().enabled(fixUniversalNumber)}; #ifdef DEBUG { diff --git a/src/ripple/app/tx/impl/apply.cpp b/src/ripple/app/tx/impl/apply.cpp index cc1e792c014..5144e05fea3 100644 --- a/src/ripple/app/tx/impl/apply.cpp +++ b/src/ripple/app/tx/impl/apply.cpp @@ -114,6 +114,7 @@ apply( beast::Journal j) { STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; + NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; auto pfresult = preflight(app, view.rules(), tx, flags, j); auto pcresult = preclaim(pfresult, app, view); diff --git a/src/ripple/basics/IOUAmount.h b/src/ripple/basics/IOUAmount.h index 7e9e50d7ee9..764aa38aae3 100644 --- a/src/ripple/basics/IOUAmount.h +++ b/src/ripple/basics/IOUAmount.h @@ -20,6 +20,8 @@ #ifndef RIPPLE_BASICS_IOUAMOUNT_H_INCLUDED #define RIPPLE_BASICS_IOUAMOUNT_H_INCLUDED +#include +#include #include #include #include @@ -56,84 +58,119 @@ class IOUAmount : private boost::totally_ordered, public: IOUAmount() = default; - IOUAmount(IOUAmount const& other) = default; - IOUAmount& - operator=(IOUAmount const& other) = default; - - IOUAmount(beast::Zero) - { - *this = beast::zero; - } + explicit IOUAmount(Number const& other); + IOUAmount(beast::Zero); + IOUAmount(std::int64_t mantissa, int exponent); - IOUAmount(std::int64_t mantissa, int exponent) - : mantissa_(mantissa), exponent_(exponent) - { - normalize(); - } + IOUAmount& operator=(beast::Zero); - IOUAmount& operator=(beast::Zero) - { - // The -100 is used to allow 0 to sort less than small positive values - // which will have a large negative exponent. - mantissa_ = 0; - exponent_ = -100; - return *this; - } + operator Number() const; IOUAmount& operator+=(IOUAmount const& other); IOUAmount& - operator-=(IOUAmount const& other) - { - *this += -other; - return *this; - } + operator-=(IOUAmount const& other); IOUAmount - operator-() const - { - return {-mantissa_, exponent_}; - } + operator-() const; bool - operator==(IOUAmount const& other) const - { - return exponent_ == other.exponent_ && mantissa_ == other.mantissa_; - } + operator==(IOUAmount const& other) const; bool operator<(IOUAmount const& other) const; /** Returns true if the amount is not zero */ - explicit operator bool() const noexcept - { - return mantissa_ != 0; - } + explicit operator bool() const noexcept; /** Return the sign of the amount */ int - signum() const noexcept - { - return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0); - } + signum() const noexcept; int - exponent() const noexcept - { - return exponent_; - } + exponent() const noexcept; std::int64_t - mantissa() const noexcept - { - return mantissa_; - } + mantissa() const noexcept; static IOUAmount minPositiveAmount(); }; +inline IOUAmount::IOUAmount(beast::Zero) +{ + *this = beast::zero; +} + +inline IOUAmount::IOUAmount(std::int64_t mantissa, int exponent) + : mantissa_(mantissa), exponent_(exponent) +{ + normalize(); +} + +inline IOUAmount& IOUAmount::operator=(beast::Zero) +{ + // The -100 is used to allow 0 to sort less than small positive values + // which will have a large negative exponent. + mantissa_ = 0; + exponent_ = -100; + return *this; +} + +inline IOUAmount::operator Number() const +{ + return Number{mantissa_, exponent_}; +} + +inline IOUAmount& +IOUAmount::operator-=(IOUAmount const& other) +{ + *this += -other; + return *this; +} + +inline IOUAmount +IOUAmount::operator-() const +{ + return {-mantissa_, exponent_}; +} + +inline bool +IOUAmount::operator==(IOUAmount const& other) const +{ + return exponent_ == other.exponent_ && mantissa_ == other.mantissa_; +} + +inline bool +IOUAmount::operator<(IOUAmount const& other) const +{ + return Number{*this} < Number{other}; +} + +inline IOUAmount::operator bool() const noexcept +{ + return mantissa_ != 0; +} + +inline int +IOUAmount::signum() const noexcept +{ + return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0); +} + +inline int +IOUAmount::exponent() const noexcept +{ + return exponent_; +} + +inline std::int64_t +IOUAmount::mantissa() const noexcept +{ + return mantissa_; +} + std::string to_string(IOUAmount const& amount); @@ -149,6 +186,31 @@ mulRatio( std::uint32_t den, bool roundUp); +extern LocalValue stNumberSwitchover; + +/** RAII class to set and restore the Number switchover. + */ + +class NumberSO +{ + bool saved_; + +public: + ~NumberSO() + { + *stNumberSwitchover = saved_; + } + + NumberSO(NumberSO const&) = delete; + NumberSO& + operator=(NumberSO const&) = delete; + + explicit NumberSO(bool v) : saved_(*stNumberSwitchover) + { + *stNumberSwitchover = v; + } +}; + } // namespace ripple #endif diff --git a/src/ripple/basics/Number.h b/src/ripple/basics/Number.h new file mode 100644 index 00000000000..c308abec712 --- /dev/null +++ b/src/ripple/basics/Number.h @@ -0,0 +1,385 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_BASICS_NUMBER_H_INCLUDED +#define RIPPLE_BASICS_NUMBER_H_INCLUDED + +#include +#include +#include +#include +#include + +namespace ripple { + +class Number; + +std::string +to_string(Number const& amount); + +class Number +{ + using rep = std::int64_t; + rep mantissa_{0}; + int exponent_{std::numeric_limits::lowest()}; + +public: + struct unchecked + { + explicit unchecked() = default; + }; + + explicit constexpr Number() = default; + + Number(rep mantissa); + explicit Number(rep mantissa, int exponent); + explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; + + Number(XRPAmount const& x); + + constexpr rep + mantissa() const noexcept; + constexpr int + exponent() const noexcept; + + constexpr Number + operator+() const noexcept; + constexpr Number + operator-() const noexcept; + Number& + operator++(); + Number + operator++(int); + Number& + operator--(); + Number + operator--(int); + + Number& + operator+=(Number const& x); + Number& + operator-=(Number const& x); + + Number& + operator*=(Number const& x); + Number& + operator/=(Number const& x); + + static constexpr Number + min() noexcept; + static constexpr Number + max() noexcept; + static constexpr Number + lowest() noexcept; + + explicit operator XRPAmount() const; // round to nearest, even on tie + explicit operator rep() const; // round to nearest, even on tie + + friend constexpr bool + operator==(Number const& x, Number const& y) noexcept + { + return x.mantissa_ == y.mantissa_ && x.exponent_ == y.exponent_; + } + + friend constexpr bool + operator!=(Number const& x, Number const& y) noexcept + { + return !(x == y); + } + + friend constexpr bool + operator<(Number const& x, Number const& y) noexcept + { + // If the two amounts have different signs (zero is treated as positive) + // then the comparison is true iff the left is negative. + bool const lneg = x.mantissa_ < 0; + bool const rneg = y.mantissa_ < 0; + + if (lneg != rneg) + return lneg; + + // Both have same sign and the left is zero: the right must be + // greater than 0. + if (x.mantissa_ == 0) + return y.mantissa_ > 0; + + // Both have same sign, the right is zero and the left is non-zero. + if (y.mantissa_ == 0) + return false; + + // Both have the same sign, compare by exponents: + if (x.exponent_ > y.exponent_) + return lneg; + if (x.exponent_ < y.exponent_) + return !lneg; + + // If equal exponents, compare mantissas + return x.mantissa_ < y.mantissa_; + } + + friend constexpr bool + operator>(Number const& x, Number const& y) noexcept + { + return y < x; + } + + friend constexpr bool + operator<=(Number const& x, Number const& y) noexcept + { + return !(y < x); + } + + friend constexpr bool + operator>=(Number const& x, Number const& y) noexcept + { + return !(x < y); + } + + friend std::ostream& + operator<<(std::ostream& os, Number const& x) + { + return os << to_string(x); + } + + // Thread local rounding control. Default is to_nearest + enum rounding_mode { to_nearest, towards_zero, downward, upward }; + static rounding_mode + getround(); + // Returns previously set mode + static rounding_mode + setround(rounding_mode mode); + +private: + static thread_local rounding_mode mode_; + + void + normalize(); + constexpr bool + isnormal() const noexcept; + + // The range for the mantissa when normalized + constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; + constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL; + + // The range for the exponent when normalized + constexpr static int minExponent = -32768; + constexpr static int maxExponent = 32768; + + class Guard; +}; + +inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept + : mantissa_{mantissa}, exponent_{exponent} +{ +} + +inline Number::Number(rep mantissa, int exponent) + : mantissa_{mantissa}, exponent_{exponent} +{ + normalize(); +} + +inline Number::Number(rep mantissa) : Number{mantissa, 0} +{ +} + +inline Number::Number(XRPAmount const& x) : Number{x.drops()} +{ +} + +inline constexpr Number::rep +Number::mantissa() const noexcept +{ + return mantissa_; +} + +inline constexpr int +Number::exponent() const noexcept +{ + return exponent_; +} + +inline constexpr Number +Number::operator+() const noexcept +{ + return *this; +} + +inline constexpr Number +Number::operator-() const noexcept +{ + auto x = *this; + x.mantissa_ = -x.mantissa_; + return x; +} + +inline Number& +Number::operator++() +{ + *this += Number{1000000000000000, -15, unchecked{}}; + return *this; +} + +inline Number +Number::operator++(int) +{ + auto x = *this; + ++(*this); + return x; +} + +inline Number& +Number::operator--() +{ + *this -= Number{1000000000000000, -15, unchecked{}}; + return *this; +} + +inline Number +Number::operator--(int) +{ + auto x = *this; + --(*this); + return x; +} + +inline Number& +Number::operator-=(Number const& x) +{ + return *this += -x; +} + +inline Number +operator+(Number const& x, Number const& y) +{ + auto z = x; + z += y; + return z; +} + +inline Number +operator-(Number const& x, Number const& y) +{ + auto z = x; + z -= y; + return z; +} + +inline Number +operator*(Number const& x, Number const& y) +{ + auto z = x; + z *= y; + return z; +} + +inline Number +operator/(Number const& x, Number const& y) +{ + auto z = x; + z /= y; + return z; +} + +inline constexpr Number +Number::min() noexcept +{ + return Number{minMantissa, minExponent, unchecked{}}; +} + +inline constexpr Number +Number::max() noexcept +{ + return Number{maxMantissa, maxExponent, unchecked{}}; +} + +inline constexpr Number +Number::lowest() noexcept +{ + return -Number{maxMantissa, maxExponent, unchecked{}}; +} + +inline constexpr bool +Number::isnormal() const noexcept +{ + auto const abs_m = mantissa_ < 0 ? -mantissa_ : mantissa_; + return minMantissa <= abs_m && abs_m <= maxMantissa && + minExponent <= exponent_ && exponent_ <= maxExponent; +} + +inline constexpr Number +abs(Number x) noexcept +{ + if (x < Number{}) + x = -x; + return x; +} + +// Returns f^n +// Uses a log_2(n) number of multiplications + +Number +power(Number const& f, unsigned n); + +// Returns f^(1/d) +// Uses Newton–Raphson iterations until the result stops changing +// to find the root of the polynomial g(x) = x^d - f + +Number +root(Number f, unsigned d); + +Number +root2(Number f); + +// Returns f^(n/d) + +Number +power(Number const& f, unsigned n, unsigned d); + +// Return 0 if abs(x) < limit, else returns x + +inline constexpr Number +squelch(Number const& x, Number const& limit) noexcept +{ + if (abs(x) < limit) + return Number{}; + return x; +} + +class saveNumberRoundMode +{ + Number::rounding_mode mode_; + +public: + ~saveNumberRoundMode() + { + Number::setround(mode_); + } + explicit saveNumberRoundMode(Number::rounding_mode mode) noexcept + : mode_{mode} + { + } + saveNumberRoundMode(saveNumberRoundMode const&) = delete; + saveNumberRoundMode& + operator=(saveNumberRoundMode const&) = delete; +}; + +} // namespace ripple + +#endif // RIPPLE_BASICS_NUMBER_H_INCLUDED diff --git a/src/ripple/basics/impl/IOUAmount.cpp b/src/ripple/basics/impl/IOUAmount.cpp index 27254662347..c9b52874abd 100644 --- a/src/ripple/basics/impl/IOUAmount.cpp +++ b/src/ripple/basics/impl/IOUAmount.cpp @@ -27,12 +27,14 @@ namespace ripple { +LocalValue stNumberSwitchover(true); + /* The range for the mantissa when normalized */ -static std::int64_t const minMantissa = 1000000000000000ull; -static std::int64_t const maxMantissa = 9999999999999999ull; +static std::int64_t constexpr minMantissa = 1000000000000000ull; +static std::int64_t constexpr maxMantissa = 9999999999999999ull; /* The range for the exponent when normalized */ -static int const minExponent = -96; -static int const maxExponent = 80; +static int constexpr minExponent = -96; +static int constexpr maxExponent = 80; IOUAmount IOUAmount::minPositiveAmount() @@ -49,6 +51,18 @@ IOUAmount::normalize() return; } + if (*stNumberSwitchover) + { + Number const v{mantissa_, exponent_}; + mantissa_ = v.mantissa(); + exponent_ = v.exponent(); + if (exponent_ > maxExponent) + Throw("value overflow"); + if (exponent_ < minExponent) + *this = beast::zero; + return; + } + bool const negative = (mantissa_ < 0); if (negative) @@ -82,6 +96,15 @@ IOUAmount::normalize() mantissa_ = -mantissa_; } +IOUAmount::IOUAmount(Number const& other) + : mantissa_(other.mantissa()), exponent_(other.exponent()) +{ + if (exponent_ > maxExponent) + Throw("value overflow"); + if (exponent_ < minExponent) + *this = beast::zero; +} + IOUAmount& IOUAmount::operator+=(IOUAmount const& other) { @@ -94,6 +117,11 @@ IOUAmount::operator+=(IOUAmount const& other) return *this; } + if (*stNumberSwitchover) + { + *this = IOUAmount{Number{*this} + Number{other}}; + return *this; + } auto m = other.mantissa_; auto e = other.exponent_; @@ -120,128 +148,13 @@ IOUAmount::operator+=(IOUAmount const& other) } normalize(); - return *this; } -bool -IOUAmount::operator<(IOUAmount const& other) const -{ - // If the two amounts have different signs (zero is treated as positive) - // then the comparison is true iff the left is negative. - bool const lneg = mantissa_ < 0; - bool const rneg = other.mantissa_ < 0; - - if (lneg != rneg) - return lneg; - - // Both have same sign and the left is zero: the right must be - // greater than 0. - if (mantissa_ == 0) - return other.mantissa_ > 0; - - // Both have same sign, the right is zero and the left is non-zero. - if (other.mantissa_ == 0) - return false; - - // Both have the same sign, compare by exponents: - if (exponent_ > other.exponent_) - return lneg; - if (exponent_ < other.exponent_) - return !lneg; - - // If equal exponents, compare mantissas - return mantissa_ < other.mantissa_; -} - std::string to_string(IOUAmount const& amount) { - // keep full internal accuracy, but make more human friendly if possible - if (amount == beast::zero) - return "0"; - - int const exponent = amount.exponent(); - auto mantissa = amount.mantissa(); - - // Use scientific notation for exponents that are too small or too large - if (((exponent != 0) && ((exponent < -25) || (exponent > -5)))) - { - std::string ret = std::to_string(mantissa); - ret.append(1, 'e'); - ret.append(std::to_string(exponent)); - return ret; - } - - bool negative = false; - - if (mantissa < 0) - { - mantissa = -mantissa; - negative = true; - } - - assert(exponent + 43 > 0); - - size_t const pad_prefix = 27; - size_t const pad_suffix = 23; - - std::string const raw_value(std::to_string(mantissa)); - std::string val; - - val.reserve(raw_value.length() + pad_prefix + pad_suffix); - val.append(pad_prefix, '0'); - val.append(raw_value); - val.append(pad_suffix, '0'); - - size_t const offset(exponent + 43); - - auto pre_from(val.begin()); - auto const pre_to(val.begin() + offset); - - auto const post_from(val.begin() + offset); - auto post_to(val.end()); - - // Crop leading zeroes. Take advantage of the fact that there's always a - // fixed amount of leading zeroes and skip them. - if (std::distance(pre_from, pre_to) > pad_prefix) - pre_from += pad_prefix; - - assert(post_to >= post_from); - - pre_from = std::find_if(pre_from, pre_to, [](char c) { return c != '0'; }); - - // Crop trailing zeroes. Take advantage of the fact that there's always a - // fixed amount of trailing zeroes and skip them. - if (std::distance(post_from, post_to) > pad_suffix) - post_to -= pad_suffix; - - assert(post_to >= post_from); - - post_to = std::find_if( - std::make_reverse_iterator(post_to), - std::make_reverse_iterator(post_from), - [](char c) { return c != '0'; }) - .base(); - - std::string ret; - - if (negative) - ret.append(1, '-'); - - // Assemble the output: - if (pre_from == pre_to) - ret.append(1, '0'); - else - ret.append(pre_from, pre_to); - - if (post_to != post_from) - { - ret.append(1, '.'); - ret.append(post_from, post_to); - } - - return ret; + return to_string(Number{amount}); } IOUAmount diff --git a/src/ripple/basics/impl/Number.cpp b/src/ripple/basics/impl/Number.cpp new file mode 100644 index 00000000000..9b3247536f9 --- /dev/null +++ b/src/ripple/basics/impl/Number.cpp @@ -0,0 +1,754 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 + +#ifdef BOOST_COMP_MSVC +#include +using uint128_t = boost::multiprecision::uint128_t; +#else // !defined(_MSVC_LANG) +using uint128_t = __uint128_t; +#endif // !defined(_MSVC_LANG) + +namespace ripple { + +thread_local Number::rounding_mode Number::mode_ = Number::to_nearest; + +Number::rounding_mode +Number::getround() +{ + return mode_; +} + +Number::rounding_mode +Number::setround(rounding_mode mode) +{ + return std::exchange(mode_, mode); +} + +// Guard + +// The Guard class is used to tempoarily add extra digits of +// preicision to an operation. This enables the final result +// to be correctly rounded to the internal precision of Number. + +class Number::Guard +{ + std::uint64_t digits_; // 16 decimal guard digits + std::uint8_t xbit_ : 1; // has a non-zero digit been shifted off the end + std::uint8_t sbit_ : 1; // the sign of the guard digits + +public: + explicit Guard() : digits_{0}, xbit_{0}, sbit_{0} + { + } + + // set & test the sign bit + void + set_positive() noexcept; + void + set_negative() noexcept; + bool + is_negative() const noexcept; + + // add a digit + void + push(unsigned d) noexcept; + + // recover a digit + unsigned + pop() noexcept; + + // Indicate round direction: 1 is up, -1 is down, 0 is even + // This enables the client to round towards nearest, and on + // tie, round towards even. + int + round() noexcept; +}; + +inline void +Number::Guard::set_positive() noexcept +{ + sbit_ = 0; +} + +inline void +Number::Guard::set_negative() noexcept +{ + sbit_ = 1; +} + +inline bool +Number::Guard::is_negative() const noexcept +{ + return sbit_ == 1; +} + +inline void +Number::Guard::push(unsigned d) noexcept +{ + xbit_ = xbit_ || (digits_ & 0x0000'0000'0000'000F) != 0; + digits_ >>= 4; + digits_ |= (d & 0x0000'0000'0000'000FULL) << 60; +} + +inline unsigned +Number::Guard::pop() noexcept +{ + unsigned d = (digits_ & 0xF000'0000'0000'0000) >> 60; + digits_ <<= 4; + return d; +} + +// Returns: +// -1 if Guard is less than half +// 0 if Guard is exactly half +// 1 if Guard is greater than half +int +Number::Guard::round() noexcept +{ + auto mode = Number::getround(); + + if (mode == towards_zero) + return -1; + + if (mode == downward) + { + if (sbit_) + { + if (digits_ > 0 || xbit_) + return 1; + } + return -1; + } + + if (mode == upward) + { + if (sbit_) + return -1; + if (digits_ > 0 || xbit_) + return 1; + return -1; + } + + // assume round to nearest if mode is not one of the predefined values + if (digits_ > 0x5000'0000'0000'0000) + return 1; + if (digits_ < 0x5000'0000'0000'0000) + return -1; + if (xbit_) + return 1; + return 0; +} + +// Number + +constexpr Number one{1000000000000000, -15, Number::unchecked{}}; + +void +Number::normalize() +{ + if (mantissa_ == 0) + { + *this = Number{}; + return; + } + bool const negative = (mantissa_ < 0); + auto m = static_cast>(mantissa_); + if (negative) + m = -m; + while ((m < minMantissa) && (exponent_ > minExponent)) + { + m *= 10; + --exponent_; + } + Guard g; + if (negative) + g.set_negative(); + while (m > maxMantissa) + { + if (exponent_ >= maxExponent) + throw std::overflow_error("Number::normalize 1"); + g.push(m % 10); + m /= 10; + ++exponent_; + } + mantissa_ = m; + if ((exponent_ < minExponent) || (mantissa_ < minMantissa)) + { + *this = Number{}; + return; + } + + auto r = g.round(); + if (r == 1 || (r == 0 && (mantissa_ & 1) == 1)) + { + ++mantissa_; + if (mantissa_ > maxMantissa) + { + mantissa_ /= 10; + ++exponent_; + } + } + if (exponent_ > maxExponent) + throw std::overflow_error("Number::normalize 2"); + + if (negative) + mantissa_ = -mantissa_; +} + +Number& +Number::operator+=(Number const& y) +{ + if (y == Number{}) + return *this; + if (*this == Number{}) + { + *this = y; + return *this; + } + if (*this == -y) + { + *this = Number{}; + return *this; + } + assert(isnormal() && y.isnormal()); + auto xm = mantissa(); + auto xe = exponent(); + int xn = 1; + if (xm < 0) + { + xm = -xm; + xn = -1; + } + auto ym = y.mantissa(); + auto ye = y.exponent(); + int yn = 1; + if (ym < 0) + { + ym = -ym; + yn = -1; + } + Guard g; + if (xe < ye) + { + if (xn == -1) + g.set_negative(); + do + { + g.push(xm % 10); + xm /= 10; + ++xe; + } while (xe < ye); + } + else if (xe > ye) + { + if (yn == -1) + g.set_negative(); + do + { + g.push(ym % 10); + ym /= 10; + ++ye; + } while (xe > ye); + } + if (xn == yn) + { + xm += ym; + if (xm > maxMantissa) + { + g.push(xm % 10); + xm /= 10; + ++xe; + } + auto r = g.round(); + if (r == 1 || (r == 0 && (xm & 1) == 1)) + { + ++xm; + if (xm > maxMantissa) + { + xm /= 10; + ++xe; + } + } + if (xe > maxExponent) + throw std::overflow_error("Number::addition overflow"); + } + else + { + if (xm > ym) + { + xm = xm - ym; + } + else + { + xm = ym - xm; + xe = ye; + xn = yn; + } + while (xm < minMantissa) + { + xm *= 10; + xm -= g.pop(); + --xe; + } + auto r = g.round(); + if (r == 1 || (r == 0 && (xm & 1) == 1)) + { + --xm; + if (xm < minMantissa) + { + xm *= 10; + --xe; + } + } + if (xe < minExponent) + { + xm = 0; + xe = Number{}.exponent_; + } + } + mantissa_ = xm * xn; + exponent_ = xe; + return *this; +} + +// Optimization equivalent to: +// auto r = static_cast(u % 10); +// u /= 10; +// return r; +// Derived from Hacker's Delight Second Edition Chapter 10 +// by Henry S. Warren, Jr. +static inline unsigned +divu10(uint128_t& u) +{ + // q = u * 0.75 + auto q = (u >> 1) + (u >> 2); + // iterate towards q = u * 0.8 + q += q >> 4; + q += q >> 8; + q += q >> 16; + q += q >> 32; + q += q >> 64; + // q /= 8 approximately == u / 10 + q >>= 3; + // r = u - q * 10 approximately == u % 10 + auto r = static_cast(u - ((q << 3) + (q << 1))); + // correction c is 1 if r >= 10 else 0 + auto c = (r + 6) >> 4; + u = q + c; + r -= c * 10; + return r; +} + +Number& +Number::operator*=(Number const& y) +{ + if (*this == Number{}) + return *this; + if (y == Number{}) + { + *this = y; + return *this; + } + assert(isnormal() && y.isnormal()); + auto xm = mantissa(); + auto xe = exponent(); + int xn = 1; + if (xm < 0) + { + xm = -xm; + xn = -1; + } + auto ym = y.mantissa(); + auto ye = y.exponent(); + int yn = 1; + if (ym < 0) + { + ym = -ym; + yn = -1; + } + auto zm = uint128_t(xm) * uint128_t(ym); + auto ze = xe + ye; + auto zn = xn * yn; + Guard g; + if (zn == -1) + g.set_negative(); + while (zm > maxMantissa) + { + // The following is optimization for: + // g.push(static_cast(zm % 10)); + // zm /= 10; + g.push(divu10(zm)); + ++ze; + } + xm = static_cast(zm); + xe = ze; + auto r = g.round(); + if (r == 1 || (r == 0 && (xm & 1) == 1)) + { + ++xm; + if (xm > maxMantissa) + { + xm /= 10; + ++xe; + } + } + if (xe < minExponent) + { + xm = 0; + xe = Number{}.exponent_; + } + if (xe > maxExponent) + throw std::overflow_error( + "Number::multiplication overflow : exponent is " + + std::to_string(xe)); + mantissa_ = xm * zn; + exponent_ = xe; + assert(isnormal() || *this == Number{}); + return *this; +} + +Number& +Number::operator/=(Number const& y) +{ + if (y == Number{}) + throw std::overflow_error("Number: divide by 0"); + if (*this == Number{}) + return *this; + int np = 1; + auto nm = mantissa(); + auto ne = exponent(); + if (nm < 0) + { + nm = -nm; + np = -1; + } + int dp = 1; + auto dm = y.mantissa(); + auto de = y.exponent(); + if (dm < 0) + { + dm = -dm; + dp = -1; + } + // Shift by 10^17 gives greatest precision while not overflowing uint128_t + // or the cast back to int64_t + const uint128_t f = 100'000'000'000'000'000; + mantissa_ = static_cast(uint128_t(nm) * f / uint128_t(dm)); + exponent_ = ne - de - 17; + mantissa_ *= np * dp; + normalize(); + return *this; +} + +Number::operator rep() const +{ + rep drops = mantissa_; + int offset = exponent_; + Guard g; + if (drops != 0) + { + if (drops < 0) + { + g.set_negative(); + drops = -drops; + } + for (; offset < 0; ++offset) + { + g.push(drops % 10); + drops /= 10; + } + for (; offset > 0; --offset) + { + if (drops > std::numeric_limits::max() / 10) + throw std::overflow_error("Number::operator rep() overflow"); + drops *= 10; + } + auto r = g.round(); + if (r == 1 || (r == 0 && (drops & 1) == 1)) + { + ++drops; + } + if (g.is_negative()) + drops = -drops; + } + return drops; +} + +Number::operator XRPAmount() const +{ + return XRPAmount{static_cast(*this)}; +} + +std::string +to_string(Number const& amount) +{ + // keep full internal accuracy, but make more human friendly if possible + if (amount == Number{}) + return "0"; + + auto const exponent = amount.exponent(); + auto mantissa = amount.mantissa(); + + // Use scientific notation for exponents that are too small or too large + if (((exponent != 0) && ((exponent < -25) || (exponent > -5)))) + { + std::string ret = std::to_string(mantissa); + ret.append(1, 'e'); + ret.append(std::to_string(exponent)); + return ret; + } + + bool negative = false; + + if (mantissa < 0) + { + mantissa = -mantissa; + negative = true; + } + + assert(exponent + 43 > 0); + + ptrdiff_t const pad_prefix = 27; + ptrdiff_t const pad_suffix = 23; + + std::string const raw_value(std::to_string(mantissa)); + std::string val; + + val.reserve(raw_value.length() + pad_prefix + pad_suffix); + val.append(pad_prefix, '0'); + val.append(raw_value); + val.append(pad_suffix, '0'); + + ptrdiff_t const offset(exponent + 43); + + auto pre_from(val.begin()); + auto const pre_to(val.begin() + offset); + + auto const post_from(val.begin() + offset); + auto post_to(val.end()); + + // Crop leading zeroes. Take advantage of the fact that there's always a + // fixed amount of leading zeroes and skip them. + if (std::distance(pre_from, pre_to) > pad_prefix) + pre_from += pad_prefix; + + assert(post_to >= post_from); + + pre_from = std::find_if(pre_from, pre_to, [](char c) { return c != '0'; }); + + // Crop trailing zeroes. Take advantage of the fact that there's always a + // fixed amount of trailing zeroes and skip them. + if (std::distance(post_from, post_to) > pad_suffix) + post_to -= pad_suffix; + + assert(post_to >= post_from); + + post_to = std::find_if( + std::make_reverse_iterator(post_to), + std::make_reverse_iterator(post_from), + [](char c) { return c != '0'; }) + .base(); + + std::string ret; + + if (negative) + ret.append(1, '-'); + + // Assemble the output: + if (pre_from == pre_to) + ret.append(1, '0'); + else + ret.append(pre_from, pre_to); + + if (post_to != post_from) + { + ret.append(1, '.'); + ret.append(post_from, post_to); + } + + return ret; +} + +// Returns f^n +// Uses a log_2(n) number of multiplications + +Number +power(Number const& f, unsigned n) +{ + if (n == 0) + return one; + if (n == 1) + return f; + auto r = power(f, n / 2); + r *= r; + if (n % 2 != 0) + r *= f; + return r; +} + +// Returns f^(1/d) +// Uses Newton–Raphson iterations until the result stops changing +// to find the non-negative root of the polynomial g(x) = x^d - f + +// This function, and power(Number f, unsigned n, unsigned d) +// treat corner cases such as 0 roots as advised by Annex F of +// the C standard, which itself is consistent with the IEEE +// floating point standards. + +Number +root(Number f, unsigned d) +{ + if (f == one || d == 1) + return f; + if (d == 0) + { + if (f == -one) + return one; + if (abs(f) < one) + return Number{}; + throw std::overflow_error("Number::root infinity"); + } + if (f < Number{} && d % 2 == 0) + throw std::overflow_error("Number::root nan"); + if (f == Number{}) + return f; + + // Scale f into the range (0, 1) such that f's exponent is a multiple of d + auto e = f.exponent() + 16; + auto const di = static_cast(d); + auto ex = [e = e, di = di]() // Euclidean remainder of e/d + { + int k = (e >= 0 ? e : e - (di - 1)) / di; + int k2 = e - k * di; + if (k2 == 0) + return 0; + return di - k2; + }(); + e += ex; + f = Number{f.mantissa(), f.exponent() - e}; // f /= 10^e; + bool neg = false; + if (f < Number{}) + { + neg = true; + f = -f; + } + + // Quadratic least squares curve fit of f^(1/d) in the range [0, 1] + auto const D = ((6 * di + 11) * di + 6) * di + 1; + auto const a0 = 3 * di * ((2 * di - 3) * di + 1); + auto const a1 = 24 * di * (2 * di - 1); + auto const a2 = -30 * (di - 1) * di; + Number r = ((Number{a2} * f + Number{a1}) * f + Number{a0}) / Number{D}; + if (neg) + { + f = -f; + r = -r; + } + + // Newton–Raphson iteration of f^(1/d) with initial guess r + // halt when r stops changing, checking for bouncing on the last iteration + Number rm1{}; + Number rm2{}; + do + { + rm2 = rm1; + rm1 = r; + r = (Number(d - 1) * r + f / power(r, d - 1)) / Number(d); + } while (r != rm1 && r != rm2); + + // return r * 10^(e/d) to reverse scaling + return Number{r.mantissa(), r.exponent() + e / di}; +} + +Number +root2(Number f) +{ + if (f == one) + return f; + if (f < Number{}) + throw std::overflow_error("Number::root nan"); + if (f == Number{}) + return f; + + // Scale f into the range (0, 1) such that f's exponent is a multiple of d + auto e = f.exponent() + 16; + if (e % 2 != 0) + ++e; + f = Number{f.mantissa(), f.exponent() - e}; // f /= 10^e; + + // Quadratic least squares curve fit of f^(1/d) in the range [0, 1] + auto const D = 105; + auto const a0 = 18; + auto const a1 = 144; + auto const a2 = -60; + Number r = ((Number{a2} * f + Number{a1}) * f + Number{a0}) / Number{D}; + + // Newton–Raphson iteration of f^(1/2) with initial guess r + // halt when r stops changing, checking for bouncing on the last iteration + Number rm1{}; + Number rm2{}; + do + { + rm2 = rm1; + rm1 = r; + r = (r + f / r) / Number(2); + } while (r != rm1 && r != rm2); + + // return r * 10^(e/2) to reverse scaling + return Number{r.mantissa(), r.exponent() + e / 2}; +} + +// Returns f^(n/d) + +Number +power(Number const& f, unsigned n, unsigned d) +{ + if (f == one) + return f; + auto g = std::gcd(n, d); + if (g == 0) + throw std::overflow_error("Number::power nan"); + if (d == 0) + { + if (f == -one) + return one; + if (abs(f) < one) + return Number{}; + // abs(f) > one + throw std::overflow_error("Number::power infinity"); + } + if (n == 0) + return one; + n /= g; + d /= g; + if ((n % 2) == 1 && (d % 2) == 0 && f < Number{}) + throw std::overflow_error("Number::power nan"); + return root(power(f, n), d); +} + +} // namespace ripple diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index b3e1dba78bd..78b5b152c87 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 55; +static constexpr std::size_t numFeatures = 56; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -342,6 +342,7 @@ extern uint256 const fixRemoveNFTokenAutoTrustLine; extern uint256 const featureImmediateOfferKilled; extern uint256 const featureDisallowIncoming; extern uint256 const featureXRPFees; +extern uint256 const fixUniversalNumber; } // namespace ripple diff --git a/src/ripple/protocol/STAmount.h b/src/ripple/protocol/STAmount.h index d0add30dbba..0b9ca953f1a 100644 --- a/src/ripple/protocol/STAmount.h +++ b/src/ripple/protocol/STAmount.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -144,6 +145,7 @@ class STAmount final : public STBase, public CountedObject // Legacy support for new-style amounts STAmount(IOUAmount const& amount, Issue const& issue); STAmount(XRPAmount const& amount); + operator Number() const; //-------------------------------------------------------------------------- // @@ -275,7 +277,13 @@ class STAmount final : public STBase, public CountedObject STBase* move(std::size_t n, void* buf) override; + STAmount& + operator=(IOUAmount const& iou); + friend class detail::STVar; + + friend STAmount + operator+(STAmount const& v1, STAmount const& v2); }; //------------------------------------------------------------------------------ @@ -370,6 +378,13 @@ inline STAmount::operator bool() const noexcept return *this != beast::zero; } +inline STAmount::operator Number() const +{ + if (mIsNative) + return xrp(); + return iou(); +} + inline STAmount& STAmount::operator=(beast::Zero) { clear(); diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 2e141c11fd1..d650950da4b 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -452,6 +452,7 @@ REGISTER_FIX (fixRemoveNFTokenAutoTrustLine, Supported::yes, DefaultVote::yes REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no); REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no); REGISTER_FEATURE(XRPFees, Supported::yes, DefaultVote::no); +REGISTER_FIX (fixUniversalNumber, Supported::yes, DefaultVote::yes); // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. diff --git a/src/ripple/protocol/impl/STAmount.cpp b/src/ripple/protocol/impl/STAmount.cpp index d764eb00d33..d1a878c8b4f 100644 --- a/src/ripple/protocol/impl/STAmount.cpp +++ b/src/ripple/protocol/impl/STAmount.cpp @@ -339,6 +339,19 @@ STAmount::iou() const return {mantissa, exponent}; } +STAmount& +STAmount::operator=(IOUAmount const& iou) +{ + assert(mIsNative == false); + mOffset = iou.exponent(); + mIsNegative = iou < beast::zero; + if (mIsNegative) + mValue = static_cast(-iou.mantissa()); + else + mValue = static_cast(iou.mantissa()); + return *this; +} + //------------------------------------------------------------------------------ // // Operators @@ -382,6 +395,13 @@ operator+(STAmount const& v1, STAmount const& v2) if (v1.native()) return {v1.getFName(), getSNValue(v1) + getSNValue(v2)}; + if (*stNumberSwitchover) + { + auto x = v1; + x = v1.iou() + v2.iou(); + return x; + } + int ov1 = v1.exponent(), ov2 = v2.exponent(); std::int64_t vv1 = static_cast(v1.mantissa()); std::int64_t vv2 = static_cast(v2.mantissa()); @@ -705,24 +725,36 @@ STAmount::canonicalize() "Native currency amount out of range"); } - while (mOffset < 0) + if (*stNumberSwitchover && *stAmountCanonicalizeSwitchover) { - mValue /= 10; - ++mOffset; + Number num( + mIsNegative ? -mValue : mValue, mOffset, Number::unchecked{}); + XRPAmount xrp{num}; + mIsNegative = xrp.drops() < 0; + mValue = mIsNegative ? -xrp.drops() : xrp.drops(); + mOffset = 0; } - - while (mOffset > 0) + else { - if (*stAmountCanonicalizeSwitchover) + while (mOffset < 0) + { + mValue /= 10; + ++mOffset; + } + + while (mOffset > 0) { - // N.B. do not move the overflow check to after the - // multiplication - if (mValue > cMaxNativeN) - Throw( - "Native currency amount out of range"); + if (*stAmountCanonicalizeSwitchover) + { + // N.B. do not move the overflow check to after the + // multiplication + if (mValue > cMaxNativeN) + Throw( + "Native currency amount out of range"); + } + mValue *= 10; + --mOffset; } - mValue *= 10; - --mOffset; } if (mValue > cMaxNativeN) @@ -733,6 +765,12 @@ STAmount::canonicalize() mIsNative = false; + if (*stNumberSwitchover) + { + *this = iou(); + return; + } + if (mValue == 0) { mOffset = -100; @@ -1170,6 +1208,9 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue) return STAmount(v1.getFName(), minV * maxV); } + if (*stNumberSwitchover) + return {IOUAmount{Number{v1} * Number{v2}}, issue}; + std::uint64_t value1 = v1.mantissa(); std::uint64_t value2 = v2.mantissa(); int offset1 = v1.exponent(); diff --git a/src/ripple/rpc/impl/ShardArchiveHandler.cpp b/src/ripple/rpc/impl/ShardArchiveHandler.cpp index 2284780c2b6..d05744f483a 100644 --- a/src/ripple/rpc/impl/ShardArchiveHandler.cpp +++ b/src/ripple/rpc/impl/ShardArchiveHandler.cpp @@ -37,11 +37,12 @@ using namespace std::chrono_literals; boost::filesystem::path ShardArchiveHandler::getDownloadDirectory(Config const& config) { - return get(config.section(ConfigSection::shardDatabase()), - "download_path", + return boost::filesystem::path{ get(config.section(ConfigSection::shardDatabase()), - "path", - "")) / + "download_path", + get(config.section(ConfigSection::shardDatabase()), + "path", + ""))} / "download"; } diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 42a6eb4d3ce..842f3f76cc8 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -2349,7 +2349,13 @@ class NFToken_test : public beast::unit_test::suite // See the impact of rounding when the nft is sold for small amounts // of drops. + for (auto NumberSwitchOver : {true}) { + if (NumberSwitchOver) + env.enableFeature(fixUniversalNumber); + else + env.disableFeature(fixUniversalNumber); + // An nft with a transfer fee of 1 basis point. uint256 const nftID = token::getNextID(env, alice, 0u, tfTransferable, 1); @@ -2374,16 +2380,16 @@ class NFToken_test : public beast::unit_test::suite // minter sells to carol. The payment is just small enough that // alice does not get any transfer fee. + auto pmt = NumberSwitchOver ? drops(50000) : drops(99999); STAmount carolBalance = env.balance(carol); uint256 const minterSellOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, drops(99999)), - txflags(tfSellNFToken)); + env(token::createOffer(minter, nftID, pmt), txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(carol, minterSellOfferIndex)); env.close(); - minterBalance += drops(99999) - fee; - carolBalance -= drops(99999) + fee; + minterBalance += pmt - fee; + carolBalance -= pmt + fee; BEAST_EXPECT(env.balance(alice) == aliceBalance); BEAST_EXPECT(env.balance(minter) == minterBalance); BEAST_EXPECT(env.balance(carol) == carolBalance); @@ -2393,13 +2399,13 @@ class NFToken_test : public beast::unit_test::suite STAmount beckyBalance = env.balance(becky); uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key; - env(token::createOffer(becky, nftID, drops(100000)), - token::owner(carol)); + pmt = NumberSwitchOver ? drops(50001) : drops(100000); + env(token::createOffer(becky, nftID, pmt), token::owner(carol)); env.close(); env(token::acceptBuyOffer(carol, beckyBuyOfferIndex)); env.close(); - carolBalance += drops(99999) - fee; - beckyBalance -= drops(100000) + fee; + carolBalance += pmt - drops(1) - fee; + beckyBalance -= pmt + fee; aliceBalance += drops(1); BEAST_EXPECT(env.balance(alice) == aliceBalance); diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 9f6e165bc16..1162612b733 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -2092,37 +2092,53 @@ class Offer_test : public beast::unit_test::suite using namespace jtx; - Env env{*this, features}; + for (auto NumberSwitchOver : {false, true}) + { + Env env{*this, features}; + if (NumberSwitchOver) + env.enableFeature(fixUniversalNumber); + else + env.disableFeature(fixUniversalNumber); - auto const gw = Account{"gateway"}; - auto const alice = Account{"alice"}; - auto const bob = Account{"bob"}; - auto const USD = gw["USD"]; + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const USD = gw["USD"]; - env.fund(XRP(10000), gw, alice, bob); + env.fund(XRP(10000), gw, alice, bob); - env(rate(gw, 1.005)); + env(rate(gw, 1.005)); - env(trust(alice, USD(1000))); - env(trust(bob, USD(1000))); - env(trust(gw, alice["USD"](50))); + env(trust(alice, USD(1000))); + env(trust(bob, USD(1000))); + env(trust(gw, alice["USD"](50))); - env(pay(gw, bob, bob["USD"](1))); - env(pay(alice, gw, USD(50))); + env(pay(gw, bob, bob["USD"](1))); + env(pay(alice, gw, USD(50))); - env(trust(gw, alice["USD"](0))); + env(trust(gw, alice["USD"](0))); - env(offer(alice, USD(50), XRP(150000))); - env(offer(bob, XRP(100), USD(0.1))); + env(offer(alice, USD(50), XRP(150000))); + env(offer(bob, XRP(100), USD(0.1))); - auto jrr = ledgerEntryState(env, alice, gw, "USD"); - BEAST_EXPECT( - jrr[jss::node][sfBalance.fieldName][jss::value] == - "49.96666666666667"); - jrr = ledgerEntryState(env, bob, gw, "USD"); - BEAST_EXPECT( - jrr[jss::node][sfBalance.fieldName][jss::value] == - "-0.966500000033334"); + auto jrr = ledgerEntryState(env, alice, gw, "USD"); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName][jss::value] == + "49.96666666666667"); + jrr = ledgerEntryState(env, bob, gw, "USD"); + if (NumberSwitchOver) + { + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName][jss::value] == + "-0.9665000000333333"); + } + else + { + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName][jss::value] == + "-0.966500000033334"); + } + } } void diff --git a/src/test/app/Taker_test.cpp b/src/test/app/Taker_test.cpp index ff0152408ff..0b69b25f24b 100644 --- a/src/test/app/Taker_test.cpp +++ b/src/test/app/Taker_test.cpp @@ -273,314 +273,521 @@ class Taker_test : public beast::unit_test::suite Quality q1 = get_quality("1", "1"); - // TAKER OWNER - // QUAL OFFER FUNDS QUAL OFFER FUNDS - // EXPECTED - // XRP USD - attempt( - Sell, - "N:N", - q1, - {"2", "2"}, - "2", - q1, - {"2", "2"}, - "2", - {"2", "2"}, - xrp(), - usd()); - attempt( - Sell, - "N:B", - q1, - {"2", "2"}, - "2", - q1, - {"2", "2"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - attempt( - Buy, - "N:T", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "N:BT", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "N:TB", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "0.8", - {"0", "0.8"}, - xrp(), - usd()); - - attempt( - Sell, - "T:N", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - xrp(), - usd()); - attempt( - Sell, - "T:B", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - attempt( - Buy, - "T:T", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "T:BT", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "1.8", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "T:TB", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "0.8", - {"0", "0.8"}, - xrp(), - usd()); - - attempt( - Sell, - "A:N", - q1, - {"2", "2"}, - "1", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - xrp(), - usd()); - attempt( - Sell, - "A:B", - q1, - {"2", "2"}, - "1", - q1, - {"2", "2"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - attempt( - Buy, - "A:T", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "3", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "A:BT", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "2.4", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "A:TB", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "0.8", - {"0", "0.8"}, - xrp(), - usd()); - - attempt( - Sell, - "TA:N", - q1, - {"2", "2"}, - "1", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - xrp(), - usd()); - attempt( - Sell, - "TA:B", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - attempt( - Buy, - "TA:T", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "3", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "TA:BT", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - attempt( - Buy, - "TA:TB", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - - attempt( - Sell, - "AT:N", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "3", - {"1", "1"}, - xrp(), - usd()); - attempt( - Sell, - "AT:B", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - attempt( - Buy, - "AT:T", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "3", - {"1", "1"}, - xrp(), - usd()); - attempt( - Buy, - "AT:BT", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "1.8", - {"1", "1.8"}, - xrp(), - usd()); - attempt( - Buy, - "AT:TB", - q1, - {"2", "2"}, - "1", - q1, - {"3", "3"}, - "0.8", - {"0", "0.8"}, - xrp(), - usd()); + for (auto NumberSwitchOver : {false, true}) + { + NumberSO stNumberSO{NumberSwitchOver}; + // TAKER OWNER + // QUAL OFFER FUNDS QUAL OFFER FUNDS + // EXPECTED + // XRP USD + attempt( + Sell, + "N:N", + q1, + {"2", "2"}, + "2", + q1, + {"2", "2"}, + "2", + {"2", "2"}, + xrp(), + usd()); + if (NumberSwitchOver) + { + attempt( + Sell, + "N:B", + q1, + {"2", "2"}, + "2", + q1, + {"2", "2"}, + "1.8", + {"2", "1.8"}, + xrp(), + usd()); + } + else + { + attempt( + Sell, + "N:B", + q1, + {"2", "2"}, + "2", + q1, + {"2", "2"}, + "1.8", + {"1", "1.8"}, + xrp(), + usd()); + } + attempt( + Buy, + "N:T", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "2", + {"1", "1"}, + xrp(), + usd()); + attempt( + Buy, + "N:BT", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "1.8", + {"1", "1"}, + xrp(), + usd()); + if (NumberSwitchOver) + { + attempt( + Buy, + "N:TB", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "0.8", + {"1", "0.8"}, + xrp(), + usd()); + } + else + { + attempt( + Buy, + "N:TB", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "0.8", + {"0", "0.8"}, + xrp(), + usd()); + } + attempt( + Sell, + "T:N", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "2", + {"1", "1"}, + xrp(), + usd()); + if (NumberSwitchOver) + { + attempt( + Sell, + "T:B", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "1.8", + {"1", "1"}, + xrp(), + usd()); + } + else + { + attempt( + Sell, + "T:B", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "1.8", + {"1", "1.8"}, + xrp(), + usd()); + } + attempt( + Buy, + "T:T", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "2", + {"1", "1"}, + xrp(), + usd()); + attempt( + Buy, + "T:BT", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "1.8", + {"1", "1"}, + xrp(), + usd()); + if (NumberSwitchOver) + { + attempt( + Buy, + "T:TB", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "0.8", + {"1", "0.8"}, + xrp(), + usd()); + } + else + { + attempt( + Buy, + "T:TB", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "0.8", + {"0", "0.8"}, + xrp(), + usd()); + } + + attempt( + Sell, + "A:N", + q1, + {"2", "2"}, + "1", + q1, + {"2", "2"}, + "2", + {"1", "1"}, + xrp(), + usd()); + if (NumberSwitchOver) + { + attempt( + Sell, + "A:B", + q1, + {"2", "2"}, + "1", + q1, + {"2", "2"}, + "1.8", + {"1", "1"}, + xrp(), + usd()); + } + else + { + attempt( + Sell, + "A:B", + q1, + {"2", "2"}, + "1", + q1, + {"2", "2"}, + "1.8", + {"1", "1.8"}, + xrp(), + usd()); + } + attempt( + Buy, + "A:T", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "3", + {"1", "1"}, + xrp(), + usd()); + attempt( + Buy, + "A:BT", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "2.4", + {"1", "1"}, + xrp(), + usd()); + if (NumberSwitchOver) + { + attempt( + Buy, + "A:TB", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "0.8", + {"1", "0.8"}, + xrp(), + usd()); + } + else + { + attempt( + Buy, + "A:TB", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "0.8", + {"0", "0.8"}, + xrp(), + usd()); + } + + attempt( + Sell, + "TA:N", + q1, + {"2", "2"}, + "1", + q1, + {"2", "2"}, + "2", + {"1", "1"}, + xrp(), + usd()); + if (NumberSwitchOver) + { + attempt( + Sell, + "TA:B", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "1.8", + {"1", "1"}, + xrp(), + usd()); + } + else + { + attempt( + Sell, + "TA:B", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "1.8", + {"1", "1.8"}, + xrp(), + usd()); + } + attempt( + Buy, + "TA:T", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "3", + {"1", "1"}, + xrp(), + usd()); + if (NumberSwitchOver) + { + attempt( + Buy, + "TA:BT", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "1.8", + {"1", "1"}, + xrp(), + usd()); + attempt( + Buy, + "TA:TB", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "1.8", + {"1", "1"}, + xrp(), + usd()); + } + else + { + attempt( + Buy, + "TA:BT", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "1.8", + {"1", "1.8"}, + xrp(), + usd()); + attempt( + Buy, + "TA:TB", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "1.8", + {"1", "1.8"}, + xrp(), + usd()); + } + + attempt( + Sell, + "AT:N", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "3", + {"1", "1"}, + xrp(), + usd()); + if (NumberSwitchOver) + { + attempt( + Sell, + "AT:B", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "1.8", + {"1", "1"}, + xrp(), + usd()); + } + else + { + attempt( + Sell, + "AT:B", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "1.8", + {"1", "1.8"}, + xrp(), + usd()); + } + attempt( + Buy, + "AT:T", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "3", + {"1", "1"}, + xrp(), + usd()); + if (NumberSwitchOver) + { + attempt( + Buy, + "AT:BT", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "1.8", + {"1", "1"}, + xrp(), + usd()); + attempt( + Buy, + "AT:TB", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "0.8", + {"1", "0.8"}, + xrp(), + usd()); + } + else + { + attempt( + Buy, + "AT:BT", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "1.8", + {"1", "1.8"}, + xrp(), + usd()); + attempt( + Buy, + "AT:TB", + q1, + {"2", "2"}, + "1", + q1, + {"3", "3"}, + "0.8", + {"0", "0.8"}, + xrp(), + usd()); + } + } } void @@ -588,316 +795,446 @@ class Taker_test : public beast::unit_test::suite { testcase("XRP Quantization: output"); - Quality q1 = get_quality("1", "1"); + for (auto NumberSwitchOver : {false, true}) + { + NumberSO stNumberSO{NumberSwitchOver}; + Quality q1 = get_quality("1", "1"); + + // TAKER OWNER + // QUAL OFFER FUNDS QUAL OFFER FUNDS + // EXPECTED + // USD XRP + attempt( + Sell, + "N:N", + q1, + {"3", "3"}, + "3", + q1, + {"3", "3"}, + "3", + {"3", "3"}, + usd(), + xrp()); + attempt( + Sell, + "N:B", + q1, + {"3", "3"}, + "3", + q1, + {"3", "3"}, + "2", + {"2", "2"}, + usd(), + xrp()); + if (NumberSwitchOver) + { + attempt( + Buy, + "N:T", + q1, + {"3", "3"}, + "2.5", + q1, + {"5", "5"}, + "5", + {"2.5", "3"}, + usd(), + xrp()); + attempt( + Buy, + "N:BT", + q1, + {"3", "3"}, + "1.5", + q1, + {"5", "5"}, + "4", + {"1.5", "2"}, + usd(), + xrp()); + } + else + { + attempt( + Buy, + "N:T", + q1, + {"3", "3"}, + "2.5", + q1, + {"5", "5"}, + "5", + {"2.5", "2"}, + usd(), + xrp()); + attempt( + Buy, + "N:BT", + q1, + {"3", "3"}, + "1.5", + q1, + {"5", "5"}, + "4", + {"1.5", "1"}, + usd(), + xrp()); + } + attempt( + Buy, + "N:TB", + q1, + {"3", "3"}, + "2.2", + q1, + {"5", "5"}, + "1", + {"1", "1"}, + usd(), + xrp()); - // TAKER OWNER - // QUAL OFFER FUNDS QUAL OFFER FUNDS - // EXPECTED - // USD XRP - attempt( - Sell, - "N:N", - q1, - {"3", "3"}, - "3", - q1, - {"3", "3"}, - "3", - {"3", "3"}, - usd(), - xrp()); - attempt( - Sell, - "N:B", - q1, - {"3", "3"}, - "3", - q1, - {"3", "3"}, - "2", - {"2", "2"}, - usd(), - xrp()); - attempt( - Buy, - "N:T", - q1, - {"3", "3"}, - "2.5", - q1, - {"5", "5"}, - "5", - {"2.5", "2"}, - usd(), - xrp()); - attempt( - Buy, - "N:BT", - q1, - {"3", "3"}, - "1.5", - q1, - {"5", "5"}, - "4", - {"1.5", "1"}, - usd(), - xrp()); - attempt( - Buy, - "N:TB", - q1, - {"3", "3"}, - "2.2", - q1, - {"5", "5"}, - "1", - {"1", "1"}, - usd(), - xrp()); - - attempt( - Sell, - "T:N", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - usd(), - xrp()); - attempt( - Sell, - "T:B", - q1, - {"2", "2"}, - "2", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - attempt( - Buy, - "T:T", - q1, - {"1", "1"}, - "2", - q1, - {"2", "2"}, - "2", - {"1", "1"}, - usd(), - xrp()); - attempt( - Buy, - "T:BT", - q1, - {"1", "1"}, - "2", - q1, - {"3", "3"}, - "2", - {"1", "1"}, - usd(), - xrp()); - attempt( - Buy, - "T:TB", - q1, - {"2", "2"}, - "2", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - - attempt( - Sell, - "A:N", - q1, - {"2", "2"}, - "1.5", - q1, - {"2", "2"}, - "2", - {"1.5", "1"}, - usd(), - xrp()); - attempt( - Sell, - "A:B", - q1, - {"2", "2"}, - "1.8", - q1, - {"3", "3"}, - "2", - {"1.8", "1"}, - usd(), - xrp()); - attempt( - Buy, - "A:T", - q1, - {"2", "2"}, - "1.2", - q1, - {"3", "3"}, - "3", - {"1.2", "1"}, - usd(), - xrp()); - attempt( - Buy, - "A:BT", - q1, - {"2", "2"}, - "1.5", - q1, - {"4", "4"}, - "3", - {"1.5", "1"}, - usd(), - xrp()); - attempt( - Buy, - "A:TB", - q1, - {"2", "2"}, - "1.5", - q1, - {"4", "4"}, - "1", - {"1", "1"}, - usd(), - xrp()); - - attempt( - Sell, - "TA:N", - q1, - {"2", "2"}, - "1.5", - q1, - {"2", "2"}, - "2", - {"1.5", "1"}, - usd(), - xrp()); - attempt( - Sell, - "TA:B", - q1, - {"2", "2"}, - "1.5", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - attempt( - Buy, - "TA:T", - q1, - {"2", "2"}, - "1.5", - q1, - {"3", "3"}, - "3", - {"1.5", "1"}, - usd(), - xrp()); - attempt( - Buy, - "TA:BT", - q1, - {"2", "2"}, - "1.8", - q1, - {"4", "4"}, - "3", - {"1.8", "1"}, - usd(), - xrp()); - attempt( - Buy, - "TA:TB", - q1, - {"2", "2"}, - "1.2", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - - attempt( - Sell, - "AT:N", - q1, - {"2", "2"}, - "2.5", - q1, - {"4", "4"}, - "4", - {"2", "2"}, - usd(), - xrp()); - attempt( - Sell, - "AT:B", - q1, - {"2", "2"}, - "2.5", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); - attempt( - Buy, - "AT:T", - q1, - {"2", "2"}, - "2.5", - q1, - {"3", "3"}, - "3", - {"2", "2"}, - usd(), - xrp()); - attempt( - Buy, - "AT:BT", - q1, - {"2", "2"}, - "2.5", - q1, - {"4", "4"}, - "3", - {"2", "2"}, - usd(), - xrp()); - attempt( - Buy, - "AT:TB", - q1, - {"2", "2"}, - "2.5", - q1, - {"3", "3"}, - "1", - {"1", "1"}, - usd(), - xrp()); + attempt( + Sell, + "T:N", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "2", + {"1", "1"}, + usd(), + xrp()); + attempt( + Sell, + "T:B", + q1, + {"2", "2"}, + "2", + q1, + {"3", "3"}, + "1", + {"1", "1"}, + usd(), + xrp()); + attempt( + Buy, + "T:T", + q1, + {"1", "1"}, + "2", + q1, + {"2", "2"}, + "2", + {"1", "1"}, + usd(), + xrp()); + attempt( + Buy, + "T:BT", + q1, + {"1", "1"}, + "2", + q1, + {"3", "3"}, + "2", + {"1", "1"}, + usd(), + xrp()); + attempt( + Buy, + "T:TB", + q1, + {"2", "2"}, + "2", + q1, + {"3", "3"}, + "1", + {"1", "1"}, + usd(), + xrp()); + + if (NumberSwitchOver) + { + attempt( + Sell, + "A:N", + q1, + {"2", "2"}, + "1.5", + q1, + {"2", "2"}, + "2", + {"1.5", "2"}, + usd(), + xrp()); + attempt( + Sell, + "A:B", + q1, + {"2", "2"}, + "1.8", + q1, + {"3", "3"}, + "2", + {"1.8", "2"}, + usd(), + xrp()); + } + else + { + attempt( + Sell, + "A:N", + q1, + {"2", "2"}, + "1.5", + q1, + {"2", "2"}, + "2", + {"1.5", "1"}, + usd(), + xrp()); + attempt( + Sell, + "A:B", + q1, + {"2", "2"}, + "1.8", + q1, + {"3", "3"}, + "2", + {"1.8", "1"}, + usd(), + xrp()); + } + attempt( + Buy, + "A:T", + q1, + {"2", "2"}, + "1.2", + q1, + {"3", "3"}, + "3", + {"1.2", "1"}, + usd(), + xrp()); + if (NumberSwitchOver) + { + attempt( + Buy, + "A:BT", + q1, + {"2", "2"}, + "1.5", + q1, + {"4", "4"}, + "3", + {"1.5", "2"}, + usd(), + xrp()); + } + else + { + attempt( + Buy, + "A:BT", + q1, + {"2", "2"}, + "1.5", + q1, + {"4", "4"}, + "3", + {"1.5", "1"}, + usd(), + xrp()); + } + attempt( + Buy, + "A:TB", + q1, + {"2", "2"}, + "1.5", + q1, + {"4", "4"}, + "1", + {"1", "1"}, + usd(), + xrp()); + + if (NumberSwitchOver) + { + attempt( + Sell, + "TA:N", + q1, + {"2", "2"}, + "1.5", + q1, + {"2", "2"}, + "2", + {"1.5", "2"}, + usd(), + xrp()); + } + else + { + attempt( + Sell, + "TA:N", + q1, + {"2", "2"}, + "1.5", + q1, + {"2", "2"}, + "2", + {"1.5", "1"}, + usd(), + xrp()); + } + attempt( + Sell, + "TA:B", + q1, + {"2", "2"}, + "1.5", + q1, + {"3", "3"}, + "1", + {"1", "1"}, + usd(), + xrp()); + if (NumberSwitchOver) + { + attempt( + Buy, + "TA:T", + q1, + {"2", "2"}, + "1.5", + q1, + {"3", "3"}, + "3", + {"1.5", "2"}, + usd(), + xrp()); + attempt( + Buy, + "TA:BT", + q1, + {"2", "2"}, + "1.8", + q1, + {"4", "4"}, + "3", + {"1.8", "2"}, + usd(), + xrp()); + } + else + { + attempt( + Buy, + "TA:T", + q1, + {"2", "2"}, + "1.5", + q1, + {"3", "3"}, + "3", + {"1.5", "1"}, + usd(), + xrp()); + attempt( + Buy, + "TA:BT", + q1, + {"2", "2"}, + "1.8", + q1, + {"4", "4"}, + "3", + {"1.8", "1"}, + usd(), + xrp()); + } + attempt( + Buy, + "TA:TB", + q1, + {"2", "2"}, + "1.2", + q1, + {"3", "3"}, + "1", + {"1", "1"}, + usd(), + xrp()); + + attempt( + Sell, + "AT:N", + q1, + {"2", "2"}, + "2.5", + q1, + {"4", "4"}, + "4", + {"2", "2"}, + usd(), + xrp()); + attempt( + Sell, + "AT:B", + q1, + {"2", "2"}, + "2.5", + q1, + {"3", "3"}, + "1", + {"1", "1"}, + usd(), + xrp()); + attempt( + Buy, + "AT:T", + q1, + {"2", "2"}, + "2.5", + q1, + {"3", "3"}, + "3", + {"2", "2"}, + usd(), + xrp()); + attempt( + Buy, + "AT:BT", + q1, + {"2", "2"}, + "2.5", + q1, + {"4", "4"}, + "3", + {"2", "2"}, + usd(), + xrp()); + attempt( + Buy, + "AT:TB", + q1, + {"2", "2"}, + "2.5", + q1, + {"3", "3"}, + "1", + {"1", "1"}, + usd(), + xrp()); + } } void @@ -905,99 +1242,143 @@ class Taker_test : public beast::unit_test::suite { testcase("IOU to IOU"); - Quality q1 = get_quality("1", "1"); + for (auto NumberSwitchOver : {false, true}) + { + NumberSO stNumberSO{NumberSwitchOver}; + Quality q1 = get_quality("1", "1"); - // Highly exaggerated 50% transfer rate for the input and output: - Rate const rate{parityRate.value + (parityRate.value / 2)}; - - // TAKER OWNER - // QUAL OFFER FUNDS QUAL OFFER FUNDS - // EXPECTED - // EUR USD - attempt( - Sell, - "N:N", - q1, - {"2", "2"}, - "10", - q1, - {"2", "2"}, - "10", - {"2", "2"}, - eur(), - usd(), - rate, - rate); - attempt( - Sell, - "N:B", - q1, - {"4", "4"}, - "10", - q1, - {"4", "4"}, - "4", - {"2.666666666666666", "2.666666666666666"}, - eur(), - usd(), - rate, - rate); - attempt( - Buy, - "N:T", - q1, - {"1", "1"}, - "10", - q1, - {"2", "2"}, - "10", - {"1", "1"}, - eur(), - usd(), - rate, - rate); - attempt( - Buy, - "N:BT", - q1, - {"2", "2"}, - "10", - q1, - {"6", "6"}, - "5", - {"2", "2"}, - eur(), - usd(), - rate, - rate); - attempt( - Buy, - "N:TB", - q1, - {"2", "2"}, - "2", - q1, - {"6", "6"}, - "1", - {"0.6666666666666667", "0.6666666666666667"}, - eur(), - usd(), - rate, - rate); - attempt( - Sell, - "A:N", - q1, - {"2", "2"}, - "2.5", - q1, - {"2", "2"}, - "10", - {"1.666666666666666", "1.666666666666666"}, - eur(), - usd(), - rate, - rate); + // Highly exaggerated 50% transfer rate for the input and output: + Rate const rate{parityRate.value + (parityRate.value / 2)}; + + // TAKER OWNER + // QUAL OFFER FUNDS QUAL OFFER FUNDS + // EXPECTED + // EUR USD + attempt( + Sell, + "N:N", + q1, + {"2", "2"}, + "10", + q1, + {"2", "2"}, + "10", + {"2", "2"}, + eur(), + usd(), + rate, + rate); + if (NumberSwitchOver) + { + attempt( + Sell, + "N:B", + q1, + {"4", "4"}, + "10", + q1, + {"4", "4"}, + "4", + {"2.666666666666667", "2.666666666666667"}, + eur(), + usd(), + rate, + rate); + } + else + { + attempt( + Sell, + "N:B", + q1, + {"4", "4"}, + "10", + q1, + {"4", "4"}, + "4", + {"2.666666666666666", "2.666666666666666"}, + eur(), + usd(), + rate, + rate); + } + attempt( + Buy, + "N:T", + q1, + {"1", "1"}, + "10", + q1, + {"2", "2"}, + "10", + {"1", "1"}, + eur(), + usd(), + rate, + rate); + attempt( + Buy, + "N:BT", + q1, + {"2", "2"}, + "10", + q1, + {"6", "6"}, + "5", + {"2", "2"}, + eur(), + usd(), + rate, + rate); + attempt( + Buy, + "N:TB", + q1, + {"2", "2"}, + "2", + q1, + {"6", "6"}, + "1", + {"0.6666666666666667", "0.6666666666666667"}, + eur(), + usd(), + rate, + rate); + if (NumberSwitchOver) + { + attempt( + Sell, + "A:N", + q1, + {"2", "2"}, + "2.5", + q1, + {"2", "2"}, + "10", + {"1.666666666666667", "1.666666666666667"}, + eur(), + usd(), + rate, + rate); + } + else + { + attempt( + Sell, + "A:N", + q1, + {"2", "2"}, + "2.5", + q1, + {"2", "2"}, + "10", + {"1.666666666666666", "1.666666666666666"}, + eur(), + usd(), + rate, + rate); + } + } } void diff --git a/src/test/app/TrustAndBalance_test.cpp b/src/test/app/TrustAndBalance_test.cpp index e67f3ba327f..0a94138ad71 100644 --- a/src/test/app/TrustAndBalance_test.cpp +++ b/src/test/app/TrustAndBalance_test.cpp @@ -411,13 +411,11 @@ class TrustAndBalance_test : public beast::unit_test::suite if (with_rate) { - // 65.00000000000001 is correct. - // This is result of limited precision. env.require(balance( alice, STAmount( carol["USD"].issue(), - 6500000000000001ull, + 6500000000000000ull, -14, false, true, diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp new file mode 100644 index 00000000000..8c12ff7c5e4 --- /dev/null +++ b/src/test/basics/Number_test.cpp @@ -0,0 +1,747 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 + +namespace ripple { + +class Number_test : public beast::unit_test::suite +{ +public: + void + testZero() + { + testcase("zero"); + + Number const z{0, 0}; + + BEAST_EXPECT(z.mantissa() == 0); + BEAST_EXPECT(z.exponent() == Number{}.exponent()); + + BEAST_EXPECT((z + z) == z); + BEAST_EXPECT((z - z) == z); + BEAST_EXPECT(z == -z); + } + + void + test_limits() + { + testcase("test_limits"); + bool caught = false; + try + { + Number x{10'000'000'000'000'000, 32768}; + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + Number x{10'000'000'000'000'000, 32767}; + BEAST_EXPECT((x == Number{1'000'000'000'000'000, 32768})); + Number z{1'000'000'000'000'000, -32769}; + BEAST_EXPECT(z == Number{}); + Number y{1'000'000'000'000'001'500, 32000}; + BEAST_EXPECT((y == Number{1'000'000'000'000'002, 32003})); + Number m{std::numeric_limits::min()}; + BEAST_EXPECT((m == Number{-9'223'372'036'854'776, 3})); + Number M{std::numeric_limits::max()}; + BEAST_EXPECT((M == Number{9'223'372'036'854'776, 3})); + caught = false; + try + { + Number q{99'999'999'999'999'999, 32767}; + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + } + + void + test_add() + { + testcase("test_add"); + using Case = std::tuple; + Case c[]{ + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'066, -15}}, + {Number{-1'000'000'000'000'000, -15}, + Number{-6'555'555'555'555'555, -29}, + Number{-1'000'000'000'000'066, -15}}, + {Number{-1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{-9'999'999'999'999'344, -16}}, + {Number{-6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'344, -16}}, + {Number{}, Number{5}, Number{5}}, + {Number{5'555'555'555'555'555, -32768}, + Number{-5'555'555'555'555'554, -32768}, + Number{0}}, + {Number{-9'999'999'999'999'999, -31}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'990, -16}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT(x + y == z); + bool caught = false; + try + { + Number{9'999'999'999'999'999, 32768} + + Number{5'000'000'000'000'000, 32767}; + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + } + + void + test_sub() + { + testcase("test_sub"); + using Case = std::tuple; + Case c[]{ + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{9'999'999'999'999'344, -16}}, + {Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{-9'999'999'999'999'344, -16}}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -15}, + Number{0}}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'001, -15}, + Number{-1'000'000'000'000'000, -30}}, + {Number{1'000'000'000'000'001, -15}, + Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -30}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT(x - y == z); + } + + void + test_mul() + { + testcase("test_mul"); + using Case = std::tuple; + saveNumberRoundMode save{Number::setround(Number::to_nearest)}; + { + Case c[]{ + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{1000000000000000, -14}}, + {Number{1000000000000000, -32768}, + Number{1000000000000000, -32768}, + Number{0}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT(x * y == z); + } + Number::setround(Number::towards_zero); + { + Case c[]{ + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{9999999999999999, -15}}, + {Number{1000000000000000, -32768}, + Number{1000000000000000, -32768}, + Number{0}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT(x * y == z); + } + Number::setround(Number::downward); + { + Case c[]{ + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{9999999999999999, -15}}, + {Number{1000000000000000, -32768}, + Number{1000000000000000, -32768}, + Number{0}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT(x * y == z); + } + Number::setround(Number::upward); + { + Case c[]{ + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{1000000000000000, -14}}, + {Number{1000000000000000, -32768}, + Number{1000000000000000, -32768}, + Number{0}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT(x * y == z); + } + bool caught = false; + try + { + Number{9'999'999'999'999'999, 32768} * + Number{5'000'000'000'000'000, 32767}; + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + } + + void + test_div() + { + testcase("test_div"); + using Case = std::tuple; + saveNumberRoundMode save{Number::setround(Number::to_nearest)}; + { + Case c[]{ + {Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT(x / y == z); + } + Number::setround(Number::towards_zero); + { + Case c[]{ + {Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'666, -16}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT(x / y == z); + } + Number::setround(Number::downward); + { + Case c[]{ + {Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'666, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT(x / y == z); + } + Number::setround(Number::upward); + { + Case c[]{ + {Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'666, -16}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT(x / y == z); + } + bool caught = false; + try + { + Number{1000000000000000, -15} / Number{0}; + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + } + + void + test_root() + { + testcase("test_root"); + using Case = std::tuple; + Case c[]{ + {Number{2}, 2, Number{1414213562373095, -15}}, + {Number{2'000'000}, 2, Number{1414213562373095, -12}}, + {Number{2, -30}, 2, Number{1414213562373095, -30}}, + {Number{-27}, 3, Number{-3}}, + {Number{1}, 5, Number{1}}, + {Number{-1}, 0, Number{1}}, + {Number{5, -1}, 0, Number{0}}, + {Number{0}, 5, Number{0}}, + {Number{5625, -4}, 2, Number{75, -2}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT((root(x, y) == z)); + bool caught = false; + try + { + (void)root(Number{-2}, 0); + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + caught = false; + try + { + (void)root(Number{-2}, 4); + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + } + + void + test_power1() + { + testcase("test_power1"); + using Case = std::tuple; + Case c[]{ + {Number{64}, 0, Number{1}}, + {Number{64}, 1, Number{64}}, + {Number{64}, 2, Number{4096}}, + {Number{-64}, 2, Number{4096}}, + {Number{64}, 3, Number{262144}}, + {Number{-64}, 3, Number{-262144}}}; + for (auto const& [x, y, z] : c) + BEAST_EXPECT((power(x, y) == z)); + } + + void + test_power2() + { + testcase("test_power2"); + using Case = std::tuple; + Case c[]{ + {Number{1}, 3, 7, Number{1}}, + {Number{-1}, 1, 0, Number{1}}, + {Number{-1, -1}, 1, 0, Number{0}}, + {Number{16}, 0, 5, Number{1}}, + {Number{34}, 3, 3, Number{34}}, + {Number{4}, 3, 2, Number{8}}}; + for (auto const& [x, n, d, z] : c) + BEAST_EXPECT((power(x, n, d) == z)); + bool caught = false; + try + { + (void)power(Number{7}, 0, 0); + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + caught = false; + try + { + (void)power(Number{7}, 1, 0); + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + caught = false; + try + { + (void)power(Number{-1, -1}, 3, 2); + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + } + + void + testConversions() + { + testcase("testConversions"); + + IOUAmount x{5, 6}; + Number y = x; + BEAST_EXPECT((y == Number{5, 6})); + IOUAmount z{y}; + BEAST_EXPECT(x == z); + XRPAmount xrp{500}; + STAmount st = xrp; + Number n = st; + BEAST_EXPECT(XRPAmount{n} == xrp); + IOUAmount x0{0, 0}; + Number y0 = x0; + BEAST_EXPECT((y0 == Number{0})); + IOUAmount z0{y0}; + BEAST_EXPECT(x0 == z0); + XRPAmount xrp0{0}; + Number n0 = xrp0; + BEAST_EXPECT(n0 == Number{0}); + XRPAmount xrp1{n0}; + BEAST_EXPECT(xrp1 == xrp0); + } + + void + test_to_integer() + { + testcase("test_to_integer"); + using Case = std::tuple; + saveNumberRoundMode save{Number::setround(Number::to_nearest)}; + { + Case c[]{ + {Number{0}, 0}, + {Number{1}, 1}, + {Number{2}, 2}, + {Number{3}, 3}, + {Number{-1}, -1}, + {Number{-2}, -2}, + {Number{-3}, -3}, + {Number{10}, 10}, + {Number{99}, 99}, + {Number{1155}, 1155}, + {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, + {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, + {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, + {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, + {Number{15, -1}, 2}, + {Number{14, -1}, 1}, + {Number{16, -1}, 2}, + {Number{25, -1}, 2}, + {Number{6, -1}, 1}, + {Number{5, -1}, 0}, + {Number{4, -1}, 0}, + {Number{-15, -1}, -2}, + {Number{-14, -1}, -1}, + {Number{-16, -1}, -2}, + {Number{-25, -1}, -2}, + {Number{-6, -1}, -1}, + {Number{-5, -1}, 0}, + {Number{-4, -1}, 0}}; + for (auto const& [x, y] : c) + { + auto j = static_cast(x); + BEAST_EXPECT(j == y); + } + } + auto prev_mode = Number::setround(Number::towards_zero); + BEAST_EXPECT(prev_mode == Number::to_nearest); + { + Case c[]{ + {Number{0}, 0}, + {Number{1}, 1}, + {Number{2}, 2}, + {Number{3}, 3}, + {Number{-1}, -1}, + {Number{-2}, -2}, + {Number{-3}, -3}, + {Number{10}, 10}, + {Number{99}, 99}, + {Number{1155}, 1155}, + {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, + {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, + {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, + {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, + {Number{15, -1}, 1}, + {Number{14, -1}, 1}, + {Number{16, -1}, 1}, + {Number{25, -1}, 2}, + {Number{6, -1}, 0}, + {Number{5, -1}, 0}, + {Number{4, -1}, 0}, + {Number{-15, -1}, -1}, + {Number{-14, -1}, -1}, + {Number{-16, -1}, -1}, + {Number{-25, -1}, -2}, + {Number{-6, -1}, 0}, + {Number{-5, -1}, 0}, + {Number{-4, -1}, 0}}; + for (auto const& [x, y] : c) + { + auto j = static_cast(x); + BEAST_EXPECT(j == y); + } + } + prev_mode = Number::setround(Number::downward); + BEAST_EXPECT(prev_mode == Number::towards_zero); + { + Case c[]{ + {Number{0}, 0}, + {Number{1}, 1}, + {Number{2}, 2}, + {Number{3}, 3}, + {Number{-1}, -1}, + {Number{-2}, -2}, + {Number{-3}, -3}, + {Number{10}, 10}, + {Number{99}, 99}, + {Number{1155}, 1155}, + {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, + {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, + {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, + {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, + {Number{15, -1}, 1}, + {Number{14, -1}, 1}, + {Number{16, -1}, 1}, + {Number{25, -1}, 2}, + {Number{6, -1}, 0}, + {Number{5, -1}, 0}, + {Number{4, -1}, 0}, + {Number{-15, -1}, -2}, + {Number{-14, -1}, -2}, + {Number{-16, -1}, -2}, + {Number{-25, -1}, -3}, + {Number{-6, -1}, -1}, + {Number{-5, -1}, -1}, + {Number{-4, -1}, -1}}; + for (auto const& [x, y] : c) + { + auto j = static_cast(x); + BEAST_EXPECT(j == y); + } + } + prev_mode = Number::setround(Number::upward); + BEAST_EXPECT(prev_mode == Number::downward); + { + Case c[]{ + {Number{0}, 0}, + {Number{1}, 1}, + {Number{2}, 2}, + {Number{3}, 3}, + {Number{-1}, -1}, + {Number{-2}, -2}, + {Number{-3}, -3}, + {Number{10}, 10}, + {Number{99}, 99}, + {Number{1155}, 1155}, + {Number{9'999'999'999'999'999, 0}, 9'999'999'999'999'999}, + {Number{9'999'999'999'999'999, 1}, 99'999'999'999'999'990}, + {Number{9'999'999'999'999'999, 2}, 999'999'999'999'999'900}, + {Number{-9'999'999'999'999'999, 2}, -999'999'999'999'999'900}, + {Number{15, -1}, 2}, + {Number{14, -1}, 2}, + {Number{16, -1}, 2}, + {Number{25, -1}, 3}, + {Number{6, -1}, 1}, + {Number{5, -1}, 1}, + {Number{4, -1}, 1}, + {Number{-15, -1}, -1}, + {Number{-14, -1}, -1}, + {Number{-16, -1}, -1}, + {Number{-25, -1}, -2}, + {Number{-6, -1}, 0}, + {Number{-5, -1}, 0}, + {Number{-4, -1}, 0}}; + for (auto const& [x, y] : c) + { + auto j = static_cast(x); + BEAST_EXPECT(j == y); + } + } + bool caught = false; + try + { + (void)static_cast(Number{9223372036854776, 3}); + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + } + + void + test_squelch() + { + testcase("test_squelch"); + Number limit{1, -6}; + BEAST_EXPECT((squelch(Number{2, -6}, limit) == Number{2, -6})); + BEAST_EXPECT((squelch(Number{1, -6}, limit) == Number{1, -6})); + BEAST_EXPECT((squelch(Number{9, -7}, limit) == Number{0})); + BEAST_EXPECT((squelch(Number{-2, -6}, limit) == Number{-2, -6})); + BEAST_EXPECT((squelch(Number{-1, -6}, limit) == Number{-1, -6})); + BEAST_EXPECT((squelch(Number{-9, -7}, limit) == Number{0})); + } + + void + testToString() + { + testcase("testToString"); + BEAST_EXPECT(to_string(Number(-2, 0)) == "-2"); + BEAST_EXPECT(to_string(Number(0, 0)) == "0"); + BEAST_EXPECT(to_string(Number(2, 0)) == "2"); + BEAST_EXPECT(to_string(Number(25, -3)) == "0.025"); + BEAST_EXPECT(to_string(Number(-25, -3)) == "-0.025"); + BEAST_EXPECT(to_string(Number(25, 1)) == "250"); + BEAST_EXPECT(to_string(Number(-25, 1)) == "-250"); + BEAST_EXPECT(to_string(Number(2, 20)) == "2000000000000000e5"); + BEAST_EXPECT(to_string(Number(-2, -20)) == "-2000000000000000e-35"); + } + + void + test_relationals() + { + testcase("test_relationals"); + BEAST_EXPECT(!(Number{100} < Number{10})); + BEAST_EXPECT(Number{100} > Number{10}); + BEAST_EXPECT(Number{100} >= Number{10}); + BEAST_EXPECT(!(Number{100} <= Number{10})); + } + + void + test_stream() + { + testcase("test_stream"); + Number x{100}; + std::ostringstream os; + os << x; + BEAST_EXPECT(os.str() == to_string(x)); + } + + void + test_inc_dec() + { + testcase("test_inc_dec"); + Number x{100}; + Number y = +x; + BEAST_EXPECT(x == y); + BEAST_EXPECT(x++ == y); + BEAST_EXPECT(x == Number{101}); + BEAST_EXPECT(x-- == Number{101}); + BEAST_EXPECT(x == y); + } + + void + test_toSTAmount() + { + NumberSO stNumberSO{true}; + Issue const issue; + Number const n{7'518'783'80596, -5}; + saveNumberRoundMode const save{Number::setround(Number::to_nearest)}; + auto res2 = STAmount{issue, n.mantissa(), n.exponent()}; + BEAST_EXPECT(res2 == STAmount{7518784}); + + Number::setround(Number::towards_zero); + res2 = STAmount{issue, n.mantissa(), n.exponent()}; + BEAST_EXPECT(res2 == STAmount{7518783}); + + Number::setround(Number::downward); + res2 = STAmount{issue, n.mantissa(), n.exponent()}; + BEAST_EXPECT(res2 == STAmount{7518783}); + + Number::setround(Number::upward); + res2 = STAmount{issue, n.mantissa(), n.exponent()}; + BEAST_EXPECT(res2 == STAmount{7518784}); + } + + void + run() override + { + testZero(); + test_limits(); + test_add(); + test_sub(); + test_mul(); + test_div(); + test_root(); + test_power1(); + test_power2(); + testConversions(); + test_to_integer(); + test_squelch(); + testToString(); + test_relationals(); + test_stream(); + test_inc_dec(); + test_toSTAmount(); + } +}; + +BEAST_DEFINE_TESTSUITE(Number, ripple_basics, ripple); + +} // namespace ripple diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 8841d2d2f58..2a85a57e1db 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -543,6 +543,9 @@ class Env void enableFeature(uint256 const feature); + void + disableFeature(uint256 const feature); + private: void fund(bool setDefaultRipple, STAmount const& amount, Account const& account); diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 900b9812dae..41eac3204ed 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -466,6 +466,14 @@ Env::enableFeature(uint256 const feature) app().config().features.insert(feature); } +void +Env::disableFeature(uint256 const feature) +{ + // Env::close() must be called for feature + // enable to take place. + app().config().features.erase(feature); +} + } // namespace jtx } // namespace test