diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 4420530239..d1ef749784 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -1,8 +1,11 @@ #ifndef XRPL_BASICS_NUMBER_H_INCLUDED #define XRPL_BASICS_NUMBER_H_INCLUDED +#include + #include #include +#include #include #include @@ -13,42 +16,252 @@ class Number; std::string to_string(Number const& amount); +template +constexpr std::optional +logTen(T value) +{ + int log = 0; + while (value >= 10 && value % 10 == 0) + { + value /= 10; + ++log; + } + if (value == 1) + return log; + return std::nullopt; +} + template constexpr bool isPowerOfTen(T value) { - while (value >= 10 && value % 10 == 0) - value /= 10; - return value == 1; + return logTen(value).has_value(); } +/** MantissaRange defines a range for the mantissa of a normalized Number. + * + * The mantissa is in the range [min, max], where + * * min is a power of 10, and + * * max = min * 10 - 1. + * + * The mantissa_scale enum indicates whether the range is "small" or "large". + * This intentionally restricts the number of MantissaRanges that can be + * instantiated to two: one for each scale. + * + * The "small" scale is based on the behavior of STAmount for IOUs. It has a min + * value of 10^15, and a max value of 10^16-1. This was sufficient for + * uses before Lending Protocol was implemented, mostly related to AMM. + * + * However, it does not have sufficient precision to represent the full integer + * range of int64_t values (-2^63 to 2^63-1), which are needed for XRP and MPT + * values. The implementation of SingleAssetVault, and LendingProtocol need to + * represent those integer values accurately and precisely, both for the + * STNumber field type, and for internal calculations. That necessitated the + * "large" scale. + * + * The "large" scale is intended to represent all values that can be represented + * by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max + * value of 10^19-1. + * + * Note that if the mentioned amendments are eventually retired, this class + * should be left in place, but the "small" scale option should be removed. This + * will allow for future expansion beyond 64-bits if it is ever needed. + */ +struct MantissaRange +{ + using rep = std::uint64_t; + enum mantissa_scale { small, large }; + + explicit constexpr MantissaRange(mantissa_scale scale_) + : min(getMin(scale_)) + , max(min * 10 - 1) + , log(logTen(min).value_or(-1)) + , scale(scale_) + { + } + + rep min; + rep max; + int log; + mantissa_scale scale; + +private: + static constexpr rep + getMin(mantissa_scale scale_) + { + switch (scale_) + { + case small: + return 1'000'000'000'000'000ULL; + case large: + return 1'000'000'000'000'000'000ULL; + default: + // Since this can never be called outside a non-constexpr + // context, this throw assures that the build fails if an + // invalid scale is used. + throw std::runtime_error("Unknown mantissa scale"); + } + } +}; + +// Like std::integral, but only 64-bit integral types. +template +concept Integral64 = + std::is_same_v || std::is_same_v; + +/** Number is a floating point type that can represent a wide range of values. + * + * It can represent all values that can be represented by an STAmount - + * regardless of asset type - XRPAmount, MPTAmount, and IOUAmount, with at least + * as much precision as those types require. + * + * ---- Internal Representation ---- + * + * Internally, Number is represented with three values: + * 1. a bool sign flag, + * 2. a std::uint64_t mantissa, + * 3. an int exponent. + * + * The internal mantissa is an unsigned integer in the range defined by the + * current MantissaRange. The exponent is an integer in the range + * [minExponent, maxExponent]. + * + * See the description of MantissaRange for more details on the ranges. + * + * A non-zero mantissa is (almost) always normalized, meaning it and the + * exponent are grown or shrunk until the mantissa is in the range + * [MantissaRange.min, MantissaRange.max]. + * + * Note: + * 1. Normalization can be disabled by using the "unchecked" ctor tag. This + * should only be used at specific conversion points, some constexpr + * values, and in unit tests. + * 2. The max of the "large" range, 10^19-1, is the largest 10^X-1 value that + * fits in an unsigned 64-bit number. (10^19-1 < 2^64-1 and + * 10^20-1 > 2^64-1). This avoids under- and overflows. + * + * ---- External Interface ---- + * + * The external interface of Number consists of a std::int64_t mantissa, which + * is restricted to 63-bits, and an int exponent, which must be in the range + * [minExponent, maxExponent]. The range of the mantissa depends on which + * MantissaRange is currently active. For the "short" range, the mantissa will + * be between 10^15 and 10^16-1. For the "large" range, the mantissa will be + * between -(2^63-1) and 2^63-1. As noted above, the "large" range is needed to + * represent the full range of valid XRP and MPT integer values accurately. + * + * Note: + * 1. 2^63-1 is between 10^18 and 10^19-1, which are the limits of the "large" + * mantissa range. + * 2. The functions mantissa() and exponent() return the external view of the + * Number value, specifically using a signed 63-bit mantissa. This may + * require altering the internal representation to fit into that range + * before the value is returned. The interface guarantees consistency of + * the two values. + * 3. Number cannot represent -2^63 (std::numeric_limits::min()) + * as an exact integer, but it doesn't need to, because all asset values + * on-ledger are non-negative. This is due to implementation details of + * several operations which use unsigned arithmetic internally. This is + * sufficient to represent all valid XRP values (where the absolute value + * can not exceed INITIAL_XRP: 10^17), and MPT values (where the absolute + * value can not exceed maxMPTokenAmount: 2^63-1). + * + * ---- Mantissa Range Switching ---- + * + * The mantissa range may be changed at runtime via setMantissaScale(). The + * default mantissa range is "large". The range is updated whenever transaction + * processing begins, based on whether SingleAssetVault or LendingProtocol are + * enabled. If either is enabled, the mantissa range is set to "large". If not, + * it is set to "small", preserving backward compatibility and correct + * "amendment-gating". + * + * It is extremely unlikely that any more calls to setMantissaScale() will be + * needed outside of unit tests. + * + * ---- Usage With Different Ranges ---- + * + * Outside of unit tests, and existing checks, code that uses Number should not + * know or care which mantissa range is active. + * + * The results of computations using Numbers with a small mantissa may differ + * from computations using Numbers with a large mantissa, specifically as it + * effects the results after rounding. That is why the large mantissa range is + * amendment gated in transaction processing. + * + * It is extremely unlikely that any more calls to getMantissaScale() will be + * needed outside of unit tests. + * + * Code that uses Number should not assume or check anything about the + * mantissa() or exponent() except that they fit into the "large" range + * specified in the "External Interface" section. + * + * ----- Unit Tests ----- + * + * Within unit tests, it may be useful to explicitly switch between the two + * ranges, or to check which range is active when checking the results of + * computations. If the test is doing the math directly, the + * set/getMantissaScale() functions may be most appropriate. However, if the + * test has anything to do with transaction processing, it should enable or + * disable the amendments that control the mantissa range choice + * (SingleAssetVault and LendingProtocol), and/or check if either of those + * amendments are enabled to determine which result to expect. + * + */ class Number { using rep = std::int64_t; - rep mantissa_{0}; + using internalrep = MantissaRange::rep; + + bool negative_{false}; + internalrep mantissa_{0}; int exponent_{std::numeric_limits::lowest()}; public: - // The range for the mantissa when normalized - constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; - static_assert(isPowerOfTen(minMantissa)); - constexpr static std::int64_t maxMantissa = minMantissa * 10 - 1; - static_assert(maxMantissa == 9'999'999'999'999'999LL); - // The range for the exponent when normalized constexpr static int minExponent = -32768; constexpr static int maxExponent = 32768; + constexpr static internalrep maxRep = std::numeric_limits::max(); + static_assert(maxRep == 9'223'372'036'854'775'807); + static_assert(-maxRep == std::numeric_limits::min() + 1); + + // May need to make unchecked private struct unchecked { explicit unchecked() = default; }; + // Like unchecked, normalized is used with the ctors that take an + // internalrep mantissa. Unlike unchecked, those ctors will normalize the + // value. + // Only unit tests are expected to use this class + struct normalized + { + explicit normalized() = default; + }; + explicit constexpr Number() = default; Number(rep mantissa); explicit Number(rep mantissa, int exponent); - explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; + explicit constexpr Number( + bool negative, + internalrep mantissa, + int exponent, + unchecked) noexcept; + // Assume unsigned values are... unsigned. i.e. positive + explicit constexpr Number( + internalrep mantissa, + int exponent, + unchecked) noexcept; + // Only unit tests are expected to use this ctor + explicit Number( + bool negative, + internalrep mantissa, + int exponent, + normalized); + // Assume unsigned values are... unsigned. i.e. positive + explicit Number(internalrep mantissa, int exponent, normalized); constexpr rep mantissa() const noexcept; @@ -78,11 +291,11 @@ public: Number& operator/=(Number const& x); - static constexpr Number + static Number min() noexcept; - static constexpr Number + static Number max() noexcept; - static constexpr Number + static Number lowest() noexcept; /** Conversions to Number are implicit and conversions away from Number @@ -96,7 +309,8 @@ public: friend constexpr bool operator==(Number const& x, Number const& y) noexcept { - return x.mantissa_ == y.mantissa_ && x.exponent_ == y.exponent_; + return x.negative_ == y.negative_ && x.mantissa_ == y.mantissa_ && + x.exponent_ == y.exponent_; } friend constexpr bool @@ -110,8 +324,8 @@ public: { // 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; + bool const lneg = x.negative_; + bool const rneg = y.negative_; if (lneg != rneg) return lneg; @@ -139,7 +353,7 @@ public: constexpr int signum() const noexcept { - return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0); + return negative_ ? -1 : (mantissa_ ? 1 : 0); } Number @@ -169,6 +383,15 @@ public: return os << to_string(x); } + friend std::string + to_string(Number const& amount); + + friend Number + root(Number f, unsigned d); + + friend Number + root2(Number f); + // Thread local rounding control. Default is to_nearest enum rounding_mode { to_nearest, towards_zero, downward, upward }; static rounding_mode @@ -177,44 +400,206 @@ public: static rounding_mode setround(rounding_mode mode); + /** Returns which mantissa scale is currently in use for normalization. + * + * If you think you need to call this outside of unit tests, no you don't. + */ + static MantissaRange::mantissa_scale + getMantissaScale(); + /** Changes which mantissa scale is used for normalization. + * + * If you think you need to call this outside of unit tests, no you don't. + */ + static void + setMantissaScale(MantissaRange::mantissa_scale scale); + + inline static internalrep + minMantissa() + { + return range_.get().min; + } + + inline static internalrep + maxMantissa() + { + return range_.get().max; + } + + inline static int + mantissaLog() + { + return range_.get().log; + } + + /// oneSmall is needed because the ranges are private + constexpr static Number + oneSmall(); + /// oneLarge is needed because the ranges are private + constexpr static Number + oneLarge(); + + // And one is needed because it needs to choose between oneSmall and + // oneLarge based on the current range + static Number + one(); + + template + [[nodiscard]] + std::pair + normalizeToRange(T minMantissa, T maxMantissa) const; + private: static thread_local rounding_mode mode_; + // The available ranges for mantissa + + constexpr static MantissaRange smallRange{MantissaRange::small}; + static_assert(isPowerOfTen(smallRange.min)); + static_assert(smallRange.min == 1'000'000'000'000'000LL); + static_assert(smallRange.max == 9'999'999'999'999'999LL); + static_assert(smallRange.log == 15); + static_assert(smallRange.min < maxRep); + static_assert(smallRange.max < maxRep); + constexpr static MantissaRange largeRange{MantissaRange::large}; + static_assert(isPowerOfTen(largeRange.min)); + static_assert(largeRange.min == 1'000'000'000'000'000'000ULL); + static_assert(largeRange.max == internalrep(9'999'999'999'999'999'999ULL)); + static_assert(largeRange.log == 18); + static_assert(largeRange.min < maxRep); + static_assert(largeRange.max > maxRep); + + // The range for the mantissa when normalized. + // Use reference_wrapper to avoid making copies, and prevent accidentally + // changing the values inside the range. + static thread_local std::reference_wrapper range_; void normalize(); - constexpr bool + + /** Normalize Number components to an arbitrary range. + * + * min/maxMantissa are parameters because this function is used by both + * normalize(), which reads from range_, and by normalizeToRange, + * which is public and can accept an arbitrary range from the caller. + */ + template + static void + normalize( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa); + + template + friend void + doNormalize( + bool& negative, + T& mantissa_, + int& exponent_, + MantissaRange::rep const& minMantissa, + MantissaRange::rep const& maxMantissa); + + bool isnormal() const noexcept; + // Copy the number, but modify the exponent by "exponentDelta". Because the + // mantissa doesn't change, the result will be "mostly" normalized, but the + // exponent could go out of range, so it will be checked. + Number + shiftExponent(int exponentDelta) const; + + // Safely convert rep (int64) mantissa to internalrep (uint64). If the rep + // is negative, returns the positive value. This takes a little extra work + // because converting std::numeric_limits::min() flirts with + // UB, and can vary across compilers. + static internalrep + externalToInternal(rep mantissa); + class Guard; }; +inline constexpr Number::Number( + bool negative, + internalrep mantissa, + int exponent, + unchecked) noexcept + : negative_(negative), mantissa_{mantissa}, exponent_{exponent} +{ +} + +inline constexpr Number::Number( + internalrep mantissa, + int exponent, + unchecked) noexcept + : Number(false, mantissa, exponent, unchecked{}) +{ +} + constexpr static Number numZero{}; -inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept - : mantissa_{mantissa}, exponent_{exponent} +inline Number::Number( + bool negative, + internalrep mantissa, + int exponent, + normalized) + : Number(negative, mantissa, exponent, unchecked{}) +{ + normalize(); +} + +inline Number::Number(internalrep mantissa, int exponent, normalized) + : Number(false, mantissa, exponent, normalized{}) { } inline Number::Number(rep mantissa, int exponent) - : mantissa_{mantissa}, exponent_{exponent} + : Number(mantissa < 0, externalToInternal(mantissa), exponent, normalized{}) { - normalize(); } inline Number::Number(rep mantissa) : Number{mantissa, 0} { } +/** Returns the mantissa of the external view of the Number. + * + * Please see the "---- External Interface ----" section of the class + * documentation for an explanation of why the internal value may be modified. + */ inline constexpr Number::rep Number::mantissa() const noexcept { - return mantissa_; + auto m = mantissa_; + if (m > maxRep) + { + XRPL_ASSERT_PARTS( + !isnormal() || (m % 10 == 0 && m / 10 <= maxRep), + "xrpl::Number::mantissa", + "large normalized mantissa has no remainder"); + m /= 10; + } + auto const sign = negative_ ? -1 : 1; + return sign * static_cast(m); } +/** Returns the exponent of the external view of the Number. + * + * Please see the "---- External Interface ----" section of the class + * documentation for an explanation of why the internal value may be modified. + */ inline constexpr int Number::exponent() const noexcept { - return exponent_; + auto e = exponent_; + if (mantissa_ > maxRep) + { + XRPL_ASSERT_PARTS( + !isnormal() || (mantissa_ % 10 == 0 && mantissa_ / 10 <= maxRep), + "xrpl::Number::exponent", + "large normalized mantissa has no remainder"); + ++e; + } + return e; } inline constexpr Number @@ -226,15 +611,17 @@ Number::operator+() const noexcept inline constexpr Number Number::operator-() const noexcept { + if (mantissa_ == 0) + return Number{}; auto x = *this; - x.mantissa_ = -x.mantissa_; + x.negative_ = !x.negative_; return x; } inline Number& Number::operator++() { - *this += Number{1000000000000000, -15, unchecked{}}; + *this += one(); return *this; } @@ -249,7 +636,7 @@ Number::operator++(int) inline Number& Number::operator--() { - *this -= Number{1000000000000000, -15, unchecked{}}; + *this -= one(); return *this; } @@ -299,30 +686,54 @@ operator/(Number const& x, Number const& y) return z; } -inline constexpr Number +inline Number Number::min() noexcept { - return Number{minMantissa, minExponent, unchecked{}}; + return Number{false, range_.get().min, minExponent, unchecked{}}; } -inline constexpr Number +inline Number Number::max() noexcept { - return Number{maxMantissa, maxExponent, unchecked{}}; + return Number{ + false, std::min(range_.get().max, maxRep), maxExponent, unchecked{}}; } -inline constexpr Number +inline Number Number::lowest() noexcept { - return -Number{maxMantissa, maxExponent, unchecked{}}; + return Number{ + true, std::min(range_.get().max, maxRep), maxExponent, unchecked{}}; } -inline constexpr bool +inline bool Number::isnormal() const noexcept { - auto const abs_m = mantissa_ < 0 ? -mantissa_ : mantissa_; - return minMantissa <= abs_m && abs_m <= maxMantissa && - minExponent <= exponent_ && exponent_ <= maxExponent; + MantissaRange const& range = range_; + auto const abs_m = mantissa_; + return *this == Number{} || + (range.min <= abs_m && abs_m <= range.max && + (abs_m <= maxRep || abs_m % 10 == 0) && minExponent <= exponent_ && + exponent_ <= maxExponent); +} + +template +std::pair +Number::normalizeToRange(T minMantissa, T maxMantissa) const +{ + bool negative = negative_; + internalrep mantissa = mantissa_; + int exponent = exponent_; + + if constexpr (std::is_unsigned_v) + XRPL_ASSERT_PARTS( + !negative, + "xrpl::Number::normalizeToRange", + "Number is non-negative for unsigned range."); + Number::normalize(negative, mantissa, exponent, minMantissa, maxMantissa); + + auto const sign = negative ? -1 : 1; + return std::make_pair(static_cast(sign * mantissa), exponent); } inline constexpr Number @@ -364,6 +775,20 @@ squelch(Number const& x, Number const& limit) noexcept return x; } +inline std::string +to_string(MantissaRange::mantissa_scale const& scale) +{ + switch (scale) + { + case MantissaRange::small: + return "small"; + case MantissaRange::large: + return "large"; + default: + throw std::runtime_error("Bad scale"); + } +} + class saveNumberRoundMode { Number::rounding_mode mode_; @@ -402,6 +827,34 @@ public: operator=(NumberRoundModeGuard const&) = delete; }; +/** Sets the new scale and restores the old scale when it leaves scope. + * + * If you think you need to use this class outside of unit tests, no you don't. + * + */ +class NumberMantissaScaleGuard +{ + MantissaRange::mantissa_scale const saved_; + +public: + explicit NumberMantissaScaleGuard( + MantissaRange::mantissa_scale scale) noexcept + : saved_{Number::getMantissaScale()} + { + Number::setMantissaScale(scale); + } + + ~NumberMantissaScaleGuard() + { + Number::setMantissaScale(saved_); + } + + NumberMantissaScaleGuard(NumberMantissaScaleGuard const&) = delete; + + NumberMantissaScaleGuard& + operator=(NumberMantissaScaleGuard const&) = delete; +}; + } // namespace xrpl #endif // XRPL_BASICS_NUMBER_H_INCLUDED diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 195e373fa0..2cdccecabb 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -121,7 +121,7 @@ toAmount( { if (isXRP(issue)) return STAmount(issue, static_cast(n)); - return STAmount(issue, n.mantissa(), n.exponent()); + return STAmount(issue, n); } else { diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index c04cb5cf70..405de18e29 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -26,8 +26,10 @@ class IOUAmount : private boost::totally_ordered, private boost::additive { private: - std::int64_t mantissa_; - int exponent_; + using mantissa_type = std::int64_t; + using exponent_type = int; + mantissa_type mantissa_; + exponent_type exponent_; /** Adjusts the mantissa and exponent to the proper range. @@ -38,11 +40,14 @@ private: void normalize(); + static IOUAmount + fromNumber(Number const& number); + public: IOUAmount() = default; explicit IOUAmount(Number const& other); IOUAmount(beast::Zero); - IOUAmount(std::int64_t mantissa, int exponent); + IOUAmount(mantissa_type mantissa, exponent_type exponent); IOUAmount& operator=(beast::Zero); @@ -71,10 +76,10 @@ public: int signum() const noexcept; - int + exponent_type exponent() const noexcept; - std::int64_t + mantissa_type mantissa() const noexcept; static IOUAmount @@ -92,7 +97,7 @@ inline IOUAmount::IOUAmount(beast::Zero) *this = beast::zero; } -inline IOUAmount::IOUAmount(std::int64_t mantissa, int exponent) +inline IOUAmount::IOUAmount(mantissa_type mantissa, exponent_type exponent) : mantissa_(mantissa), exponent_(exponent) { normalize(); @@ -149,13 +154,13 @@ IOUAmount::signum() const noexcept return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0); } -inline int +inline IOUAmount::exponent_type IOUAmount::exponent() const noexcept { return exponent_; } -inline std::int64_t +inline IOUAmount::mantissa_type IOUAmount::mantissa() const noexcept { return mantissa_; diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index 519d7a96f3..a76b7a8316 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -37,6 +37,9 @@ public: bool native() const; + bool + integral() const; + friend constexpr std::weak_ordering operator<=>(Issue const& lhs, Issue const& rhs); }; diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index b89b59ee0d..ca81548a29 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -46,6 +46,12 @@ public: { return false; } + + bool + integral() const + { + return true; + } }; constexpr bool diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 0c72b80de4..43be9d3b45 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -233,6 +233,7 @@ std::size_t constexpr maxMPTokenMetadataLength = 1024; /** The maximum amount of MPTokenIssuance */ std::uint64_t constexpr maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull; +static_assert(Number::maxRep >= maxMPTokenAmount); /** The maximum length of Data payload */ std::size_t constexpr maxDataPayloadLength = 256; diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index b1d353196d..7f404b4d5f 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -135,7 +135,10 @@ public: sMD_Always = 0x10, // value when node containing it is affected at all sMD_BaseTen = 0x20, // value is treated as base 10, overriding behavior sMD_PseudoAccount = 0x40, // if this field is set in an ACCOUNT_ROOT - // _only_, then it is a pseudo-account + // _only_, then it is a pseudo-account + sMD_NeedsAsset = 0x80, // This field needs to be associated with an + // asset before it is serialized as a ledger + // object. Intended for STNumber. sMD_Default = sMD_ChangeOrig | sMD_ChangeNew | sMD_DeleteFinal | sMD_Create }; diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 79cbf51436..4d86aed2ec 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -138,7 +138,7 @@ public: template STAmount(A const& asset, Number const& number) - : STAmount(asset, number.mantissa(), number.exponent()) + : STAmount(fromNumber(asset, number)) { } @@ -282,6 +282,10 @@ public: mpt() const; private: + template + static STAmount + fromNumber(A const& asset, Number const& number); + static std::unique_ptr construct(SerialIter&, SField const& name); @@ -345,10 +349,19 @@ STAmount::STAmount( , mIsNegative(negative) { // mValue is uint64, but needs to fit in the range of int64 - XRPL_ASSERT( - mValue <= std::numeric_limits::max(), - "xrpl::STAmount::STAmount(SField, A, std::uint64_t, int, bool) : " - "maximum mantissa input"); + if (Number::getMantissaScale() == MantissaRange::small) + { + XRPL_ASSERT( + mValue <= std::numeric_limits::max(), + "xrpl::STAmount::STAmount(SField, A, std::uint64_t, int, bool) : " + "maximum mantissa input"); + } + else + { + if (integral() && mValue > std::numeric_limits::max()) + throw std::overflow_error( + "STAmount mantissa is too large " + std::to_string(mantissa)); + } canonicalize(); } @@ -542,14 +555,23 @@ STAmount::operator=(XRPAmount const& amount) return *this; } -inline STAmount& -STAmount::operator=(Number const& number) +template +inline STAmount +STAmount::fromNumber(A const& a, Number const& number) { - mIsNegative = number.mantissa() < 0; - mValue = mIsNegative ? -number.mantissa() : number.mantissa(); - mOffset = number.exponent(); - canonicalize(); - return *this; + bool const negative = number.mantissa() < 0; + Number const working{negative ? -number : number}; + Asset asset{a}; + if (asset.integral()) + { + std::uint64_t const intValue = static_cast(working); + return STAmount{asset, intValue, 0, negative}; + } + + auto const [mantissa, exponent] = + working.normalizeToRange(cMinValue, cMaxValue); + + return STAmount{asset, mantissa, exponent, negative}; } inline void @@ -699,17 +721,32 @@ getRate(STAmount const& offerOut, STAmount const& offerIn); * @param rounding Optional Number rounding mode * */ -STAmount +[[nodiscard]] STAmount roundToScale( STAmount const& value, std::int32_t scale, Number::rounding_mode rounding = Number::getround()); +/** Round an arbitrary precision Number IN PLACE to the precision of a given + * Asset. + * + * This is used to ensure that calculations do not collect dust for IOUs, or + * fractional amounts for the integral types XRP and MPT. + * + * @param asset The relevant asset + * @param value The lvalue to be rounded + */ +template +void +roundToAsset(A const& asset, Number& value) +{ + value = STAmount{asset, value}; +} + /** Round an arbitrary precision Number to the precision of a given Asset. * - * This is used to ensure that calculations do not collect dust beyond the - * precision of the reference value for IOUs, or fractional amounts for the - * integral types XRP and MPT. + * This is used to ensure that calculations do not collect dust beyond specified + * scale for IOUs, or fractional amounts for the integral types XRP and MPT. * * @param asset The relevant asset * @param value The value to be rounded @@ -718,7 +755,7 @@ roundToScale( * @param rounding Optional Number rounding mode */ template -Number +[[nodiscard]] Number roundToAsset( A const& asset, Number const& value, diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h index dfdb16af93..39b0c3b042 100644 --- a/include/xrpl/protocol/STNumber.h +++ b/include/xrpl/protocol/STNumber.h @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -19,8 +20,19 @@ namespace xrpl { * it can represent a value of any token type (XRP, IOU, or MPT) * without paying the storage cost of duplicating asset information * that may be deduced from the context. + * + * STNumber derives from STTakesAsset, so that it can be associated with the + * related Asset during transaction processing. Which asset is relevant depends + * on the object and transaction. As of this writing, only Vault, LoanBroker, + * and Loan objects use STNumber fields. All of those fields represent amounts + * of the Vault's Asset, so they should be associated with the Vault's Asset. + * + * e.g. + * associateAsset(*loanSle, asset); + * associateAsset(*brokerSle, asset); + * associateAsset(*vaultSle, asset); */ -class STNumber : public STBase, public CountedObject +class STNumber : public STTakesAsset, public CountedObject { private: Number value_; @@ -56,6 +68,9 @@ public: bool isDefault() const override; + void + associateAsset(Asset const& a) override; + operator Number() const { return value_; diff --git a/include/xrpl/protocol/STTakesAsset.h b/include/xrpl/protocol/STTakesAsset.h new file mode 100644 index 0000000000..767223b97d --- /dev/null +++ b/include/xrpl/protocol/STTakesAsset.h @@ -0,0 +1,63 @@ +#ifndef XRPL_PROTOCOL_STTAKESASSET_H_INCLUDED +#define XRPL_PROTOCOL_STTAKESASSET_H_INCLUDED + +#include +#include + +namespace xrpl { + +/** Intermediate class for any STBase-derived class to store an Asset. + * + * In the class definition, this class should be specified as a base class + * _instead_ of STBase. + * + * Specifically, the Asset is only stored and used at runtime. It should not be + * serialized to the ledger. + * + * The derived class decides what to do with the Asset, and when. It will not + * necessarily be set at any given time. As of this writing, only STNumber uses + * it to round the stored Number to the Asset's precision both when associated, + * and when serializing the Number. + */ +class STTakesAsset : public STBase +{ +protected: + std::optional asset_; + +public: + using STBase::STBase; + using STBase::operator=; + + virtual void + associateAsset(Asset const& a); +}; + +inline void +STTakesAsset::associateAsset(Asset const& a) +{ + asset_.emplace(a); +} + +class STLedgerEntry; + +/** Associate an Asset with all sMD_NeedsAsset fields in a ledger entry. + * + * This function iterates over all fields in the given ledger entry. For each + * field that is set and has the SField::sMD_NeedsAsset metadata flag, it calls + * `associateAsset` on that field with the given Asset. Such field must be + * derived from STTakesAsset - if it is not, the conversion will throw. + * + * Typically, associateAsset should be called near the end of doApply() of any + * Transactor classes on the SLEs of any new or modified ledger entries + * containing STNumber fields, after doing all of the modifications t the SLEs. + * + * @param sle The ledger entry whose fields will be updated. + * @param asset The Asset to associate with the relevant fields. + * + */ +void +associateAsset(STLedgerEntry& sle, Asset const& asset); + +} // namespace xrpl + +#endif diff --git a/include/xrpl/protocol/SystemParameters.h b/include/xrpl/protocol/SystemParameters.h index c0732bc9fe..c2f66e9ea1 100644 --- a/include/xrpl/protocol/SystemParameters.h +++ b/include/xrpl/protocol/SystemParameters.h @@ -23,6 +23,8 @@ systemName() /** Number of drops in the genesis account. */ constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP}; +static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000); +static_assert(Number::maxRep >= INITIAL_XRP.drops()); /** Returns true if the amount does not exceed the initial XRP in existence. */ inline bool diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index d0736469e4..712cf568af 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -207,22 +207,22 @@ TYPED_SFIELD(sfLoanID, UINT256, 38) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) -TYPED_SFIELD(sfAssetsAvailable, NUMBER, 2) -TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3) -TYPED_SFIELD(sfAssetsTotal, NUMBER, 4) -TYPED_SFIELD(sfLossUnrealized, NUMBER, 5) -TYPED_SFIELD(sfDebtTotal, NUMBER, 6) -TYPED_SFIELD(sfDebtMaximum, NUMBER, 7) -TYPED_SFIELD(sfCoverAvailable, NUMBER, 8) +TYPED_SFIELD(sfAssetsAvailable, NUMBER, 2, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfAssetsTotal, NUMBER, 4, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfLossUnrealized, NUMBER, 5, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfDebtTotal, NUMBER, 6, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfDebtMaximum, NUMBER, 7, SField::sMD_NeedsAsset | SField::sMD_Default) +TYPED_SFIELD(sfCoverAvailable, NUMBER, 8, SField::sMD_NeedsAsset | SField::sMD_Default) TYPED_SFIELD(sfLoanOriginationFee, NUMBER, 9) TYPED_SFIELD(sfLoanServiceFee, NUMBER, 10) TYPED_SFIELD(sfLatePaymentFee, NUMBER, 11) TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12) -TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) +TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13, SField::sMD_NeedsAsset | SField::sMD_Default) TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) -TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15) +TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::sMD_NeedsAsset | SField::sMD_Default) TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16) -TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17) +TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::sMD_NeedsAsset | SField::sMD_Default) // int32 TYPED_SFIELD(sfLoanScale, INT32, 1) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 9984b26ffe..436ebf6779 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -1,4 +1,6 @@ #include +// Keep Number.h first to ensure it can build without hidden dependencies +#include #include #include @@ -13,16 +15,20 @@ #include #ifdef _MSC_VER -#pragma message("Using boost::multiprecision::uint128_t") +#pragma message("Using boost::multiprecision::uint128_t and int128_t") #include using uint128_t = boost::multiprecision::uint128_t; +using int128_t = boost::multiprecision::int128_t; #else // !defined(_MSC_VER) using uint128_t = __uint128_t; +using int128_t = __int128_t; #endif // !defined(_MSC_VER) namespace xrpl { thread_local Number::rounding_mode Number::mode_ = Number::to_nearest; +thread_local std::reference_wrapper Number::range_ = + largeRange; Number::rounding_mode Number::getround() @@ -36,12 +42,30 @@ Number::setround(rounding_mode mode) return std::exchange(mode_, mode); } +MantissaRange::mantissa_scale +Number::getMantissaScale() +{ + return range_.get().scale; +} + +void +Number::setMantissaScale(MantissaRange::mantissa_scale scale) +{ + if (scale != MantissaRange::small && scale != MantissaRange::large) + LogicError("Unknown mantissa scale"); + range_ = scale == MantissaRange::small ? smallRange : largeRange; +} + // Guard // The Guard class is used to temporarily add extra digits of // precision to an operation. This enables the final result // to be correctly rounded to the internal precision of Number. +template +concept UnsignedMantissa = + std::is_unsigned_v || std::is_same_v; + class Number::Guard { std::uint64_t digits_; // 16 decimal guard digits @@ -62,8 +86,9 @@ public: is_negative() const noexcept; // add a digit + template void - push(unsigned d) noexcept; + push(T d) noexcept; // recover a digit unsigned @@ -76,16 +101,40 @@ public: round() noexcept; // Modify the result to the correctly rounded value + template void - doRoundUp(rep& mantissa, int& exponent, std::string location); + doRoundUp( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa, + std::string location); + + // Modify the result to the correctly rounded value + template + void + doRoundDown( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa); // Modify the result to the correctly rounded value void - doRoundDown(rep& mantissa, int& exponent); + doRound(rep& drops, std::string location); - // Modify the result to the correctly rounded value +private: void - doRound(rep& drops); + doPush(unsigned d) noexcept; + + template + void + bringIntoRange( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa); }; inline void @@ -107,13 +156,20 @@ Number::Guard::is_negative() const noexcept } inline void -Number::Guard::push(unsigned d) noexcept +Number::Guard::doPush(unsigned d) noexcept { - xbit_ = xbit_ || (digits_ & 0x0000'0000'0000'000F) != 0; + xbit_ = xbit_ || ((digits_ & 0x0000'0000'0000'000F) != 0); digits_ >>= 4; digits_ |= (d & 0x0000'0000'0000'000FULL) << 60; } +template +inline void +Number::Guard::push(T d) noexcept +{ + doPush(static_cast(d)); +} + inline unsigned Number::Guard::pop() noexcept { @@ -163,30 +219,65 @@ Number::Guard::round() noexcept return 0; } +template void -Number::Guard::doRoundUp(rep& mantissa, int& exponent, std::string location) +Number::Guard::bringIntoRange( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa) +{ + // Bring mantissa back into the minMantissa / maxMantissa range AFTER + // rounding + if (mantissa < minMantissa) + { + mantissa *= 10; + --exponent; + } + if (exponent < minExponent) + { + constexpr Number zero = Number{}; + + negative = zero.negative_; + mantissa = zero.mantissa_; + exponent = zero.exponent_; + } +} + +template +void +Number::Guard::doRoundUp( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa, + std::string location) { auto r = round(); if (r == 1 || (r == 0 && (mantissa & 1) == 1)) { ++mantissa; - if (mantissa > maxMantissa) + // Ensure mantissa after incrementing fits within both the + // min/maxMantissa range and is a valid "rep". + if (mantissa > maxMantissa || mantissa > maxRep) { mantissa /= 10; ++exponent; } } - if (exponent < minExponent) - { - mantissa = 0; - exponent = Number{}.exponent_; - } + bringIntoRange(negative, mantissa, exponent, minMantissa); if (exponent > maxExponent) throw std::overflow_error(location); } +template void -Number::Guard::doRoundDown(rep& mantissa, int& exponent) +Number::Guard::doRoundDown( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa) { auto r = round(); if (r == 1 || (r == 0 && (mantissa & 1) == 1)) @@ -198,20 +289,27 @@ Number::Guard::doRoundDown(rep& mantissa, int& exponent) --exponent; } } - if (exponent < minExponent) - { - mantissa = 0; - exponent = Number{}.exponent_; - } + bringIntoRange(negative, mantissa, exponent, minMantissa); } // Modify the result to the correctly rounded value void -Number::Guard::doRound(rep& drops) +Number::Guard::doRound(rep& drops, std::string location) { auto r = round(); if (r == 1 || (r == 0 && (drops & 1) == 1)) { + if (drops >= maxRep) + { + static_assert(sizeof(internalrep) == sizeof(rep)); + // This should be impossible, because it's impossible to represent + // "maxRep + 0.6" in Number, regardless of the scale. There aren't + // enough digits available. You'd either get a mantissa of "maxRep" + // or "(maxRep + 1) / 10", neither of which will round up when + // converting to rep, though the latter might overflow _before_ + // rounding. + throw std::overflow_error(location); // LCOV_EXCL_LINE + } ++drops; } if (is_negative()) @@ -220,20 +318,88 @@ Number::Guard::doRound(rep& drops) // Number -constexpr Number one{1000000000000000, -15, Number::unchecked{}}; - -void -Number::normalize() +// Safely convert rep (int64) mantissa to internalrep (uint64). If the rep is +// negative, returns the positive value. This takes a little extra work because +// converting std::numeric_limits::min() flirts with UB, and can +// vary across compilers. +Number::internalrep +Number::externalToInternal(rep mantissa) { + // If the mantissa is already positive, just return it + if (mantissa >= 0) + return mantissa; + // If the mantissa is negative, but fits within the positive range of rep, + // return it negated + if (mantissa >= -std::numeric_limits::max()) + return -mantissa; + + // If the mantissa doesn't fit within the positive range, convert to + // int128_t, negate that, and cast it back down to the internalrep + // In practice, this is only going to cover the case of + // std::numeric_limits::min(). + int128_t temp = mantissa; + return static_cast(-temp); +} + +constexpr Number +Number::oneSmall() +{ + return Number{ + false, + Number::smallRange.min, + -Number::smallRange.log, + Number::unchecked{}}; +}; + +constexpr Number oneSml = Number::oneSmall(); + +constexpr Number +Number::oneLarge() +{ + return Number{ + false, + Number::largeRange.min, + -Number::largeRange.log, + Number::unchecked{}}; +}; + +constexpr Number oneLrg = Number::oneLarge(); + +Number +Number::one() +{ + if (&range_.get() == &smallRange) + return oneSml; + XRPL_ASSERT(&range_.get() == &largeRange, "Number::one() : valid range_"); + return oneLrg; +} + +// Use the member names in this static function for now so the diff is cleaner +// TODO: Rename the function parameters to get rid of the "_" suffix +template +void +doNormalize( + bool& negative, + T& mantissa_, + int& exponent_, + MantissaRange::rep const& minMantissa, + MantissaRange::rep const& maxMantissa) +{ + auto constexpr minExponent = Number::minExponent; + auto constexpr maxExponent = Number::maxExponent; + auto constexpr maxRep = Number::maxRep; + + using Guard = Number::Guard; + + constexpr Number zero = Number{}; if (mantissa_ == 0) { - *this = Number{}; + mantissa_ = zero.mantissa_; + exponent_ = zero.exponent_; + negative = zero.negative_; return; } - bool const negative = (mantissa_ < 0); - auto m = static_cast>(mantissa_); - if (negative) - m = -m; + auto m = mantissa_; while ((m < minMantissa) && (exponent_ > minExponent)) { m *= 10; @@ -250,57 +416,161 @@ Number::normalize() m /= 10; ++exponent_; } - mantissa_ = m; - if ((exponent_ < minExponent) || (mantissa_ < minMantissa)) + if ((exponent_ < minExponent) || (m < minMantissa)) { - *this = Number{}; + mantissa_ = zero.mantissa_; + exponent_ = zero.exponent_; + negative = zero.negative_; return; } - g.doRoundUp(mantissa_, exponent_, "Number::normalize 2"); + // When using the largeRange, "m" needs fit within an int64, even if + // the final mantissa_ is going to end up larger to fit within the + // MantissaRange. Cut it down here so that the rounding will be done while + // it's smaller. + // + // Example: 9,900,000,000,000,123,456 > 9,223,372,036,854,775,807, + // so "m" will be modified to 990,000,000,000,012,345. Then that value + // will be rounded to 990,000,000,000,012,345 or + // 990,000,000,000,012,346, depending on the rounding mode. Finally, + // mantissa_ will be "m*10" so it fits within the range, and end up as + // 9,900,000,000,000,123,450 or 9,900,000,000,000,123,460. + // mantissa() will return mantissa_ / 10, and exponent() will return + // exponent_ + 1. + if (m > maxRep) + { + if (exponent_ >= maxExponent) + throw std::overflow_error("Number::normalize 1.5"); + g.push(m % 10); + m /= 10; + ++exponent_; + } + // Before modification, m should be within the min/max range. After + // modification, it must be less than maxRep. In other words, the original + // value should have been no more than maxRep * 10. + // (maxRep * 10 > maxMantissa) + XRPL_ASSERT_PARTS( + m <= maxRep, + "xrpl::doNormalize", + "intermediate mantissa fits in int64"); + mantissa_ = m; - if (negative) - mantissa_ = -mantissa_; + g.doRoundUp( + negative, + mantissa_, + exponent_, + minMantissa, + maxMantissa, + "Number::normalize 2"); + XRPL_ASSERT_PARTS( + mantissa_ >= minMantissa && mantissa_ <= maxMantissa, + "xrpl::doNormalize", + "final mantissa fits in range"); +} + +template <> +void +Number::normalize( + bool& negative, + uint128_t& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa) +{ + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa); +} + +template <> +void +Number::normalize( + bool& negative, + unsigned long long& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa) +{ + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa); +} + +template <> +void +Number::normalize( + bool& negative, + unsigned long& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa) +{ + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa); +} + +void +Number::normalize() +{ + auto const& range = range_.get(); + normalize(negative_, mantissa_, exponent_, range.min, range.max); +} + +// Copy the number, but set a new exponent. Because the mantissa doesn't change, +// the result will be "mostly" normalized, but the exponent could go out of +// range. +Number +Number::shiftExponent(int exponentDelta) const +{ + XRPL_ASSERT_PARTS(isnormal(), "xrpl::Number::shiftExponent", "normalized"); + auto const newExponent = exponent_ + exponentDelta; + if (newExponent >= maxExponent) + throw std::overflow_error("Number::shiftExponent"); + if (newExponent < minExponent) + { + return Number{}; + } + Number const result{negative_, mantissa_, newExponent, unchecked{}}; + XRPL_ASSERT_PARTS( + result.isnormal(), + "xrpl::Number::shiftExponent", + "result is normalized"); + return result; } Number& Number::operator+=(Number const& y) { - if (y == Number{}) + constexpr Number zero = Number{}; + if (y == zero) return *this; - if (*this == Number{}) + if (*this == zero) { *this = y; return *this; } if (*this == -y) { - *this = Number{}; + *this = zero; return *this; } + XRPL_ASSERT( isnormal() && y.isnormal(), "xrpl::Number::operator+=(Number) : is normal"); - 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; - } + // *n = negative + // *s = sign + // *m = mantissa + // *e = exponent + + // Need to use uint128_t, because large mantissas can overflow when added + // together. + bool xn = negative_; + uint128_t xm = mantissa_; + auto xe = exponent_; + + bool yn = y.negative_; + uint128_t ym = y.mantissa_; + auto ye = y.exponent_; Guard g; if (xe < ye) { - if (xn == -1) + if (xn) g.set_negative(); do { @@ -311,7 +581,7 @@ Number::operator+=(Number const& y) } else if (xe > ye) { - if (yn == -1) + if (yn) g.set_negative(); do { @@ -320,16 +590,22 @@ Number::operator+=(Number const& y) ++ye; } while (xe > ye); } + + auto const& range = range_.get(); + auto const& minMantissa = range.min; + auto const& maxMantissa = range.max; + if (xn == yn) { xm += ym; - if (xm > maxMantissa) + if (xm > maxMantissa || xm > maxRep) { g.push(xm % 10); xm /= 10; ++xe; } - g.doRoundUp(xm, xe, "Number::addition overflow"); + g.doRoundUp( + xn, xm, xe, minMantissa, maxMantissa, "Number::addition overflow"); } else { @@ -343,16 +619,19 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - while (xm < minMantissa) + while (xm < minMantissa && xm * 10 <= maxRep) { xm *= 10; xm -= g.pop(); --xe; } - g.doRoundDown(xm, xe); + g.doRoundDown(xn, xm, xe, minMantissa); } - mantissa_ = xm * xn; + + negative_ = xn; + mantissa_ = static_cast(xm); exponent_ = xe; + normalize(); return *this; } @@ -387,39 +666,42 @@ divu10(uint128_t& u) Number& Number::operator*=(Number const& y) { - if (*this == Number{}) + constexpr Number zero = Number{}; + if (*this == zero) return *this; - if (y == Number{}) + if (y == zero) { *this = y; return *this; } - XRPL_ASSERT( - isnormal() && y.isnormal(), - "xrpl::Number::operator*=(Number) : is normal"); - 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; - } + // *n = negative + // *s = sign + // *m = mantissa + // *e = exponent + + bool xn = negative_; + int xs = xn ? -1 : 1; + internalrep xm = mantissa_; + auto xe = exponent_; + + bool yn = y.negative_; + int ys = yn ? -1 : 1; + internalrep ym = y.mantissa_; + auto ye = y.exponent_; + auto zm = uint128_t(xm) * uint128_t(ym); auto ze = xe + ye; - auto zn = xn * yn; + auto zs = xs * ys; + bool zn = (zs == -1); Guard g; - if (zn == -1) + if (zn) g.set_negative(); - while (zm > maxMantissa) + + auto const& range = range_.get(); + auto const& minMantissa = range.min; + auto const& maxMantissa = range.max; + + while (zm > maxMantissa || zm > maxRep) { // The following is optimization for: // g.push(static_cast(zm % 10)); @@ -427,61 +709,129 @@ Number::operator*=(Number const& y) g.push(divu10(zm)); ++ze; } - xm = static_cast(zm); + xm = static_cast(zm); xe = ze; g.doRoundUp( + zn, xm, xe, + minMantissa, + maxMantissa, "Number::multiplication overflow : exponent is " + std::to_string(xe)); - mantissa_ = xm * zn; + negative_ = zn; + mantissa_ = xm; exponent_ = xe; - XRPL_ASSERT( - isnormal() || *this == Number{}, - "xrpl::Number::operator*=(Number) : result is normal"); + + normalize(); return *this; } Number& Number::operator/=(Number const& y) { - if (y == Number{}) + constexpr Number zero = Number{}; + if (y == zero) throw std::overflow_error("Number: divide by 0"); - if (*this == Number{}) + if (*this == zero) return *this; - int np = 1; - auto nm = mantissa(); - auto ne = exponent(); - if (nm < 0) + // n* = numerator + // d* = denominator + // *p = negative (positive?) + // *s = sign + // *m = mantissa + // *e = exponent + + bool np = negative_; + int ns = (np ? -1 : 1); + auto nm = mantissa_; + auto ne = exponent_; + + bool dp = y.negative_; + int ds = (dp ? -1 : 1); + auto dm = y.mantissa_; + auto de = y.exponent_; + + auto const& range = range_.get(); + auto const& minMantissa = range.min; + auto const& maxMantissa = range.max; + + // Shift by 10^17 gives greatest precision while not overflowing + // uint128_t or the cast back to int64_t + // TODO: Can/should this be made bigger for largeRange? + // log(2^128,10) ~ 38.5 + // largeRange.log = 18, fits in 10^19 + // f can be up to 10^(38-19) = 10^19 safely + static_assert(smallRange.log == 15); + static_assert(largeRange.log == 18); + bool small = Number::getMantissaScale() == MantissaRange::small; + uint128_t const f = + small ? 100'000'000'000'000'000 : 10'000'000'000'000'000'000ULL; + XRPL_ASSERT_PARTS( + f >= minMantissa * 10, "Number::operator/=", "factor expected size"); + + // unsigned denominator + auto const dmu = static_cast(dm); + // correctionFactor can be anything between 10 and f, depending on how much + // extra precision we want to only use for rounding with the + // largeRange. Three digits seems like plenty, and is more than + // the smallRange uses. + uint128_t const correctionFactor = 1'000; + + auto const numerator = uint128_t(nm) * f; + + auto zm = numerator / dmu; + auto ze = ne - de - (small ? 17 : 19); + bool zn = (ns * ds) < 0; + if (!small) { - nm = -nm; - np = -1; + // Virtually multiply numerator by correctionFactor. Since that would + // overflow in the existing uint128_t, we'll do that part separately. + // The math for this would work for small mantissas, but we need to + // preserve existing behavior. + // + // Consider: + // ((numerator * correctionFactor) / dmu) / correctionFactor + // = ((numerator / dmu) * correctionFactor) / correctionFactor) + // + // But that assumes infinite precision. With integer math, this is + // equivalent to + // + // = ((numerator / dmu * correctionFactor) + // + ((numerator % dmu) * correctionFactor) / dmu) / correctionFactor + // + // We have already set `mantissa_ = numerator / dmu`. Now we + // compute `remainder = numerator % dmu`, and if it is + // nonzero, we do the rest of the arithmetic. If it's zero, we can skip + // it. + auto const remainder = (numerator % dmu); + if (remainder != 0) + { + zm *= correctionFactor; + auto const correction = remainder * correctionFactor / dmu; + zm += correction; + // divide by 1000 by moving the exponent, so we don't lose the + // integer value we just computed + ze -= 3; + } } - 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 - uint128_t const 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(); + normalize(zn, zm, ze, minMantissa, maxMantissa); + negative_ = zn; + mantissa_ = static_cast(zm); + exponent_ = ze; + XRPL_ASSERT_PARTS( + isnormal(), "xrpl::Number::operator/=", "result is normalized"); + return *this; } Number::operator rep() const { - rep drops = mantissa_; - int offset = exponent_; + rep drops = mantissa(); + int offset = exponent(); Guard g; if (drops != 0) { - if (drops < 0) + if (negative_) { g.set_negative(); drops = -drops; @@ -493,11 +843,11 @@ Number::operator rep() const } for (; offset > 0; --offset) { - if (drops > std::numeric_limits::max() / 10) + if (drops > maxRep / 10) throw std::overflow_error("Number::operator rep() overflow"); drops *= 10; } - g.doRound(drops); + g.doRound(drops, "Number::operator rep() rounding overflow"); } return drops; } @@ -524,34 +874,37 @@ std::string to_string(Number const& amount) { // keep full internal accuracy, but make more human friendly if possible - if (amount == Number{}) + constexpr Number zero = Number{}; + if (amount == zero) return "0"; - auto const exponent = amount.exponent(); - auto mantissa = amount.mantissa(); + auto exponent = amount.exponent_; + auto mantissa = amount.mantissa_; + bool const negative = amount.negative_; // Use scientific notation for exponents that are too small or too large - if (((exponent != 0) && ((exponent < -25) || (exponent > -5)))) + auto const rangeLog = Number::mantissaLog(); + if (((exponent != 0) && + ((exponent < -(rangeLog + 10)) || (exponent > -(rangeLog - 10))))) { - std::string ret = std::to_string(mantissa); + while (mantissa != 0 && mantissa % 10 == 0 && + exponent < Number::maxExponent) + { + mantissa /= 10; + ++exponent; + } + std::string ret = negative ? "-" : ""; + ret.append(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; - } - XRPL_ASSERT( exponent + 43 > 0, "xrpl::to_string(Number) : minimum exponent"); - ptrdiff_t const pad_prefix = 27; - ptrdiff_t const pad_suffix = 23; + ptrdiff_t const pad_prefix = rangeLog + 12; + ptrdiff_t const pad_suffix = rangeLog + 8; std::string const raw_value(std::to_string(mantissa)); std::string val; @@ -561,7 +914,7 @@ to_string(Number const& amount) val.append(raw_value); val.append(pad_suffix, '0'); - ptrdiff_t const offset(exponent + 43); + ptrdiff_t const offset(exponent + pad_prefix + rangeLog + 1); auto pre_from(val.begin()); auto const pre_to(val.begin() + offset); @@ -621,7 +974,7 @@ Number power(Number const& f, unsigned n) { if (n == 0) - return one; + return Number::one(); if (n == 1) return f; auto r = power(f, n / 2); @@ -643,6 +996,9 @@ power(Number const& f, unsigned n) Number root(Number f, unsigned d) { + constexpr Number zero = Number{}; + auto const one = Number::one(); + if (f == one || d == 1) return f; if (d == 0) @@ -650,16 +1006,16 @@ root(Number f, unsigned d) if (f == -one) return one; if (abs(f) < one) - return Number{}; + return zero; throw std::overflow_error("Number::root infinity"); } - if (f < Number{} && d % 2 == 0) + if (f < zero && d % 2 == 0) throw std::overflow_error("Number::root nan"); - if (f == Number{}) + if (f == zero) 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 e = f.exponent_ + Number::mantissaLog() + 1; auto const di = static_cast(d); auto ex = [e = e, di = di]() // Euclidean remainder of e/d { @@ -670,9 +1026,12 @@ root(Number f, unsigned d) return di - k2; }(); e += ex; - f = Number{f.mantissa(), f.exponent() - e}; // f /= 10^e; + f = f.shiftExponent(-e); // f /= 10^e; + + XRPL_ASSERT_PARTS( + f.isnormal(), "xrpl::root(Number, unsigned)", "f is normalized"); bool neg = false; - if (f < Number{}) + if (f < zero) { neg = true; f = -f; @@ -702,24 +1061,33 @@ root(Number f, unsigned d) } while (r != rm1 && r != rm2); // return r * 10^(e/d) to reverse scaling - return Number{r.mantissa(), r.exponent() + e / di}; + auto const result = r.shiftExponent(e / di); + XRPL_ASSERT_PARTS( + result.isnormal(), + "xrpl::root(Number, unsigned)", + "result is normalized"); + return result; } Number root2(Number f) { + constexpr Number zero = Number{}; + auto const one = Number::one(); + if (f == one) return f; - if (f < Number{}) + if (f < zero) throw std::overflow_error("Number::root nan"); - if (f == Number{}) + if (f == zero) 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 e = f.exponent_ + Number::mantissaLog() + 1; if (e % 2 != 0) ++e; - f = Number{f.mantissa(), f.exponent() - e}; // f /= 10^e; + f = f.shiftExponent(-e); // f /= 10^e; + XRPL_ASSERT_PARTS(f.isnormal(), "xrpl::root2(Number)", "f is normalized"); // Quadratic least squares curve fit of f^(1/d) in the range [0, 1] auto const D = 105; @@ -740,7 +1108,11 @@ root2(Number f) } while (r != rm1 && r != rm2); // return r * 10^(e/2) to reverse scaling - return Number{r.mantissa(), r.exponent() + e / 2}; + auto const result = r.shiftExponent(e / 2); + XRPL_ASSERT_PARTS( + result.isnormal(), "xrpl::root2(Number)", "result is normalized"); + + return result; } // Returns f^(n/d) @@ -748,6 +1120,9 @@ root2(Number f) Number power(Number const& f, unsigned n, unsigned d) { + constexpr Number zero = Number{}; + auto const one = Number::one(); + if (f == one) return f; auto g = std::gcd(n, d); @@ -758,7 +1133,7 @@ power(Number const& f, unsigned n, unsigned d) if (f == -one) return one; if (abs(f) < one) - return Number{}; + return zero; // abs(f) > one throw std::overflow_error("Number::power infinity"); } @@ -766,7 +1141,7 @@ power(Number const& f, unsigned n, unsigned d) return one; n /= g; d /= g; - if ((n % 2) == 1 && (d % 2) == 0 && f < Number{}) + if ((n % 2) == 1 && (d % 2) == 0 && f < zero) throw std::overflow_error("Number::power nan"); return root(power(f, n), d); } diff --git a/src/libxrpl/protocol/IOUAmount.cpp b/src/libxrpl/protocol/IOUAmount.cpp index 5c9ab1febc..297c2bac12 100644 --- a/src/libxrpl/protocol/IOUAmount.cpp +++ b/src/libxrpl/protocol/IOUAmount.cpp @@ -1,8 +1,11 @@ +#include +// Do not remove. Forces IOUAmount.h to stay first, to verify it can compile +// without any hidden dependencies #include #include #include #include -#include +#include #include @@ -40,11 +43,24 @@ setSTNumberSwitchover(bool v) } /* The range for the mantissa when normalized */ -static std::int64_t constexpr minMantissa = 1000000000000000ull; -static std::int64_t constexpr maxMantissa = 9999999999999999ull; +// log(2^63,10) ~ 18.96 +// +static std::int64_t constexpr minMantissa = STAmount::cMinValue; +static std::int64_t constexpr maxMantissa = STAmount::cMaxValue; /* The range for the exponent when normalized */ -static int constexpr minExponent = -96; -static int constexpr maxExponent = 80; +static int constexpr minExponent = STAmount::cMinOffset; +static int constexpr maxExponent = STAmount::cMaxOffset; + +IOUAmount +IOUAmount::fromNumber(Number const& number) +{ + // Need to create a default IOUAmount and assign directly so it doesn't try + // to normalize, which calls fromNumber + IOUAmount result{}; + std::tie(result.mantissa_, result.exponent_) = + number.normalizeToRange(minMantissa, maxMantissa); + return result; +} IOUAmount IOUAmount::minPositiveAmount() @@ -64,8 +80,7 @@ IOUAmount::normalize() if (getSTNumberSwitchover()) { Number const v{mantissa_, exponent_}; - mantissa_ = v.mantissa(); - exponent_ = v.exponent(); + *this = fromNumber(v); if (exponent_ > maxExponent) Throw("value overflow"); if (exponent_ < minExponent) @@ -106,8 +121,7 @@ IOUAmount::normalize() mantissa_ = -mantissa_; } -IOUAmount::IOUAmount(Number const& other) - : mantissa_(other.mantissa()), exponent_(other.exponent()) +IOUAmount::IOUAmount(Number const& other) : IOUAmount(fromNumber(other)) { if (exponent_ > maxExponent) Throw("value overflow"); diff --git a/src/libxrpl/protocol/Issue.cpp b/src/libxrpl/protocol/Issue.cpp index b858a31e3e..ca5bf35e8b 100644 --- a/src/libxrpl/protocol/Issue.cpp +++ b/src/libxrpl/protocol/Issue.cpp @@ -49,6 +49,12 @@ Issue::native() const return *this == xrpIssue(); } +bool +Issue::integral() const +{ + return native(); +} + bool isConsistent(Issue const& ac) { diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index b1f2c2d631..3710322699 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -1,10 +1,13 @@ +#include +// Do not remove. Forces Rules.h to stay first, to verify it can compile +// without any hidden dependencies #include +#include #include #include #include #include #include -#include #include #include @@ -33,6 +36,15 @@ getCurrentTransactionRules() void setCurrentTransactionRules(std::optional r) { + // Make global changes associated with the rules before the value is moved. + // Push the appropriate setting, instead of having the class pull every time + // the value is needed. That could get expensive fast. + bool enableLargeNumbers = !r || + (r->enabled(featureSingleAssetVault) || + r->enabled(featureLendingProtocol)); + Number::setMantissaScale( + enableLargeNumbers ? MantissaRange::large : MantissaRange::small); + *getCurrentTransactionRulesRef() = std::move(r); } diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index ebccfb3e64..ec60971e63 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -11,11 +11,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -310,7 +312,8 @@ STAmount& STAmount::operator=(IOUAmount const& iou) { XRPL_ASSERT( - native() == false, "xrpl::STAmount::operator=(IOUAmount) : is not XRP"); + integral() == false, + "xrpl::STAmount::operator=(IOUAmount) : is not integral"); mOffset = iou.exponent(); mIsNegative = iou < beast::zero; if (mIsNegative) @@ -320,6 +323,26 @@ STAmount::operator=(IOUAmount const& iou) return *this; } +STAmount& +STAmount::operator=(Number const& number) +{ + if (!getCurrentTransactionRules() || + isFeatureEnabled(featureSingleAssetVault) || + isFeatureEnabled(featureLendingProtocol)) + { + *this = fromNumber(mAsset, number); + } + else + { + auto const originalMantissa = number.mantissa(); + mIsNegative = originalMantissa < 0; + mValue = mIsNegative ? -originalMantissa : originalMantissa; + mOffset = number.exponent(); + } + canonicalize(); + return *this; +} + //------------------------------------------------------------------------------ // // Operators @@ -849,11 +872,11 @@ STAmount::canonicalize() if (getSTNumberSwitchover()) { - Number num( - mIsNegative ? -mValue : mValue, mOffset, Number::unchecked{}); + Number num(mIsNegative, mValue, mOffset, Number::unchecked{}); auto set = [&](auto const& val) { - mIsNegative = val.value() < 0; - mValue = mIsNegative ? -val.value() : val.value(); + auto const value = val.value(); + mIsNegative = value < 0; + mValue = mIsNegative ? -value : value; }; if (native()) set(XRPAmount{num}); @@ -1323,7 +1346,7 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) if (getSTNumberSwitchover()) { auto const r = Number{v1} * Number{v2}; - return STAmount{asset, r.mantissa(), r.exponent()}; + return STAmount{asset, r}; } std::uint64_t value1 = v1.mantissa(); @@ -1471,6 +1494,10 @@ roundToScale( if (value.integral()) return value; + // Nothing to do for zero. + if (value == beast::zero) + return value; + // If the value's exponent is greater than or equal to the scale, then // rounding will do nothing, and might even lose precision, so just return // the value. diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index f85bb48e0a..2f2dae7493 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -1,9 +1,13 @@ +#include +// Do not remove. Keep STNumber.h first #include #include #include +#include #include +#include #include -#include +#include #include #include @@ -17,11 +21,11 @@ namespace xrpl { STNumber::STNumber(SField const& field, Number const& value) - : STBase(field), value_(value) + : STTakesAsset(field), value_(value) { } -STNumber::STNumber(SerialIter& sit, SField const& field) : STBase(field) +STNumber::STNumber(SerialIter& sit, SField const& field) : STTakesAsset(field) { // We must call these methods in separate statements // to guarantee their order of execution. @@ -42,6 +46,19 @@ STNumber::getText() const return to_string(value_); } +void +STNumber::associateAsset(Asset const& a) +{ + STTakesAsset::associateAsset(a); + + XRPL_ASSERT_PARTS( + getFName().shouldMeta(SField::sMD_NeedsAsset), + "STNumber::associateAsset", + "field needs asset"); + + roundToAsset(a, value_); +} + void STNumber::add(Serializer& s) const { @@ -49,8 +66,49 @@ STNumber::add(Serializer& s) const XRPL_ASSERT( getFName().fieldType == getSType(), "xrpl::STNumber::add : field type match"); - s.add64(value_.mantissa()); - s.add32(value_.exponent()); + + auto value = value_; + auto const mantissa = value.mantissa(); + auto const exponent = value.exponent(); + + SField const& field = getFName(); + if (field.shouldMeta(SField::sMD_NeedsAsset)) + { + // asset is defined in the STTakesAsset base class + if (asset_) + { + // The number should be rounded to the asset's precision, but round + // it here if it has an asset assigned. + roundToAsset(*asset_, value); + XRPL_ASSERT_PARTS( + value_ == value, + "xrpl::STNumber::add", + "value is already rounded"); + } + else + { +#if !NDEBUG + // There are circumstances where an already-rounded Number is + // serialized without being touched by a transactor, and thus + // without an asset. We can't know if it's rounded, because it could + // represent _anything_, particularly when serializing user-provided + // Json. Regardless, the only time we should be serializing an + // STNumber is when the scale is large. + XRPL_ASSERT_PARTS( + Number::getMantissaScale() == MantissaRange::large, + "xrpl::STNumber::add", + "STNumber only used with large mantissa scale"); +#endif + } + } + + XRPL_ASSERT_PARTS( + mantissa <= std::numeric_limits::max() && + mantissa >= std::numeric_limits::min(), + "xrpl::STNumber::add", + "mantissa in valid range"); + s.add64(mantissa); + s.add32(exponent); } Number const& @@ -179,20 +237,30 @@ numberFromJson(SField const& field, Json::Value const& value) else if (value.isString()) { parts = partsFromString(value.asString()); - // Only strings can represent out-of-range values. - if (parts.mantissa > std::numeric_limits::max()) - Throw("too high"); + + XRPL_ASSERT_PARTS( + !getCurrentTransactionRules(), + "xrpld::numberFromJson", + "Not in a Transactor context"); + + // Number mantissas are much bigger than the allowable parsed values, so + // it can't be out of range. + static_assert( + std::numeric_limits::max() >= + std::numeric_limits::max()); } else { Throw("not a number"); } - std::int64_t mantissa = parts.mantissa; - if (parts.negative) - mantissa = -mantissa; - - return STNumber{field, Number{mantissa, parts.exponent}}; + return STNumber{ + field, + Number{ + parts.negative, + parts.mantissa, + parts.exponent, + Number::normalized{}}}; } } // namespace xrpl diff --git a/src/libxrpl/protocol/STTakesAsset.cpp b/src/libxrpl/protocol/STTakesAsset.cpp new file mode 100644 index 0000000000..d43e7b04a1 --- /dev/null +++ b/src/libxrpl/protocol/STTakesAsset.cpp @@ -0,0 +1,29 @@ +#include +// Do not remove. Force STTakesAsset.h first +#include + +namespace xrpl { + +void +associateAsset(SLE& sle, Asset const& asset) +{ + // Iterating by offset is the only way to get non-const references + for (int i = 0; i < sle.getCount(); ++i) + { + STBase& entry = sle.getIndex(i); + SField const& field = entry.getFName(); + if (field.shouldMeta(SField::sMD_NeedsAsset)) + { + auto const type = entry.getSType(); + // If the field is not set or present, skip it. + if (type == STI_NOTPRESENT) + continue; + // If the type doesn't downcast, then the flag shouldn't be on the + // SField + auto& ta = entry.downcast(); + ta.associateAsset(asset); + } + } +} + +} // namespace xrpl diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 93fda8fe34..52f05f9ed5 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -2425,7 +2425,10 @@ class AMMClawback_test : public beast::unit_test::suite void run() override { - FeatureBitset const all = jtx::testable_amendments(); + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + FeatureBitset const all = jtx::testable_amendments() - + featureSingleAssetVault - featureLendingProtocol; testInvalidRequest(); testFeatureDisabled(all - featureAMMClawback); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 317f6cb63d..d1816df51b 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -26,6 +26,9 @@ namespace test { */ struct AMMExtended_test : public jtx::AMMTest { + // Use small Number mantissas for the life of this test. + NumberMantissaScaleGuard const sg_{xrpl::MantissaRange::small}; + private: void testRmFundedOffer(FeatureBitset features) @@ -42,6 +45,7 @@ private: // funded and not used for the payment. using namespace jtx; + Env env{*this, features}; fund( @@ -1418,7 +1422,12 @@ private: testOffers() { using namespace jtx; - FeatureBitset const all{testable_amendments()}; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + FeatureBitset const all{ + testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; + testRmFundedOffer(all); testRmFundedOffer(all - fixAMMv1_1 - fixAMMv1_3); testEnforceNoRipple(all); @@ -3746,7 +3755,11 @@ private: testFlow() { using namespace jtx; - FeatureBitset const all{testable_amendments()}; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas in the transaction engine + FeatureBitset const all{ + testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; testFalseDry(all); testBookStep(all); @@ -3760,7 +3773,11 @@ private: testCrossingLimits() { using namespace jtx; - FeatureBitset const all{testable_amendments()}; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas in the transaction engine + FeatureBitset const all{ + testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; testStepLimit(all); testStepLimit(all - fixAMMv1_1 - fixAMMv1_3); } @@ -3769,7 +3786,11 @@ private: testDeliverMin() { using namespace jtx; - FeatureBitset const all{testable_amendments()}; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas in the transaction engine + FeatureBitset const all{ + testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; test_convert_all_of_an_asset(all); test_convert_all_of_an_asset(all - fixAMMv1_1 - fixAMMv1_3); } @@ -3777,7 +3798,12 @@ private: void testDepositAuth() { - testPayment(jtx::testable_amendments()); + // For now, just disable SAV entirely, which locks in the small Number + // mantissas in the transaction engine + FeatureBitset const all{ + jtx::testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; + testPayment(all); testPayIOU(); } @@ -3785,7 +3811,11 @@ private: testFreeze() { using namespace test::jtx; - auto const sa = testable_amendments(); + // For now, just disable SAV entirely, which locks in the small Number + // mantissas in the transaction engine + FeatureBitset const sa{ + testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; testRippleState(sa); testGlobalFreeze(sa); testOffersWhenFrozen(sa); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 468d5b3ffd..55bf4aa0a3 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -30,7 +30,19 @@ namespace test { */ struct AMM_test : public jtx::AMMTest { + // Use small Number mantissas for the life of this test. + NumberMantissaScaleGuard const sg_{xrpl::MantissaRange::small}; + private: + static FeatureBitset + testable_amendments() + { + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + return jtx::testable_amendments() - featureSingleAssetVault - + featureLendingProtocol; + } + void testInstanceCreate() { @@ -38,6 +50,7 @@ private: using namespace jtx; +#if NUMBERTODO // XRP to IOU, with featureSingleAssetVault testAMM( [&](AMM& ammAlice, Env&) { @@ -48,6 +61,7 @@ private: 0, {}, {testable_amendments() | featureSingleAssetVault}); +#endif // XRP to IOU, without featureSingleAssetVault testAMM( @@ -1365,8 +1379,8 @@ private: { testcase("Deposit"); - using namespace jtx; auto const all = testable_amendments(); + using namespace jtx; // Equal deposit: 1000000 tokens, 10% of the current pool testAMM([&](AMM& ammAlice, Env& env) { @@ -1384,15 +1398,14 @@ private: // equal asset deposit: unit test to exercise the rounding-down of // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations // The LPTokens need to have 16 significant digits and a fractional part - for (Number const deltaLPTokens : + for (Number const& deltaLPTokens : {Number{UINT64_C(100000'0000000009), -10}, Number{UINT64_C(100000'0000000001), -10}}) { testAMM([&](AMM& ammAlice, Env& env) { // initial LPToken balance IOUAmount const initLPToken = ammAlice.getLPTokensBalance(); - IOUAmount const newLPTokens{ - deltaLPTokens.mantissa(), deltaLPTokens.exponent()}; + IOUAmount const newLPTokens{deltaLPTokens}; // carol performs a two-asset deposit ammAlice.deposit( @@ -1417,11 +1430,9 @@ private: Number const deltaXRP = fr * 1e10; Number const deltaUSD = fr * 1e4; - STAmount const depositUSD = - STAmount{USD, deltaUSD.mantissa(), deltaUSD.exponent()}; + STAmount const depositUSD = STAmount{USD, deltaUSD}; - STAmount const depositXRP = - STAmount{XRP, deltaXRP.mantissa(), deltaXRP.exponent()}; + STAmount const depositXRP = STAmount{XRP, deltaXRP}; // initial LPTokens (1e7) + newLPTokens BEAST_EXPECT(ammAlice.expectBalances( @@ -1487,7 +1498,7 @@ private: }); // Single deposit: 100000 tokens worth of XRP - testAMM([&](AMM& ammAlice, Env&) { + testAMM([&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 100'000, XRP(205)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'201), USD(10'000), IOUAmount{10'100'000, 0})); @@ -1668,8 +1679,8 @@ private: { testcase("Invalid Withdraw"); - using namespace jtx; auto const all = testable_amendments(); + using namespace jtx; testAMM( [&](AMM& ammAlice, Env& env) { @@ -2248,8 +2259,8 @@ private: { testcase("Withdraw"); - using namespace jtx; auto const all = testable_amendments(); + using namespace jtx; // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current // pool @@ -2669,8 +2680,8 @@ private: testFeeVote() { testcase("Fee Vote"); - using namespace jtx; auto const all = testable_amendments(); + using namespace jtx; // One vote sets fee to 1%. testAMM([&](AMM& ammAlice, Env& env) { @@ -3014,6 +3025,10 @@ private: using namespace jtx; using namespace std::chrono; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + features = features - featureSingleAssetVault - featureLendingProtocol; + // Auction slot initially is owned by AMM creator, who pays 0 price. // Bid 110 tokens. Pay bidMin. @@ -3758,6 +3773,11 @@ private: testcase("Basic Payment"); using namespace jtx; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + features = features - featureSingleAssetVault - featureLendingProtocol - + featureLendingProtocol; + // Payment 100USD for 100XRP. // Force one path with tfNoRippleDirect. testAMM( @@ -4836,12 +4856,12 @@ private: testAmendment() { testcase("Amendment"); - using namespace jtx; FeatureBitset const all{testable_amendments()}; FeatureBitset const noAMM{all - featureAMM}; FeatureBitset const noNumber{all - fixUniversalNumber}; FeatureBitset const noAMMAndNumber{ all - featureAMM - fixUniversalNumber}; + using namespace jtx; for (auto const& feature : {noAMM, noNumber, noAMMAndNumber}) { @@ -6476,6 +6496,8 @@ private: Env env(*this, features, std::make_unique(&logs)); auto rules = env.current()->rules(); CurrentTransactionRulesGuard rg(rules); + NumberMantissaScaleGuard sg(MantissaRange::small); + for (auto const& t : tests) { auto getPool = [&](std::string const& v, bool isXRP) { @@ -7025,7 +7047,7 @@ private: {{xrpPool, iouPool}}, 889, std::nullopt, - {jtx::testable_amendments() | fixAMMv1_1}); + {testable_amendments() | fixAMMv1_1}); } void @@ -7566,6 +7588,7 @@ private: { auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR); + NumberMantissaScaleGuard sg(MantissaRange::small); NumberRoundModeGuard g( env.enabled(fixAMMv1_3) ? Number::upward : Number::getround()); auto const res = root2(amount * amount2); @@ -7880,7 +7903,7 @@ private: void run() override { - FeatureBitset const all{jtx::testable_amendments()}; + FeatureBitset const all{testable_amendments()}; testInvalidInstance(); testInstanceCreate(); testInvalidDeposit(all); diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index 955ca8f449..589a8b474e 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -559,12 +559,15 @@ struct EscrowToken_test : public beast::unit_test::suite env(pay(gw, bob, USD(1))); env.close(); + bool const largeMantissa = features[featureSingleAssetVault] || + features[featureLendingProtocol]; + // alice cannot create escrow for 1/10 iou - precision loss env(escrow::create(alice, bob, USD(1)), escrow::condition(escrow::cb1), escrow::finish_time(env.now() + 1s), fee(baseFee * 150), - ter(tecPRECISION_LOSS)); + ter(largeMantissa ? (TER)tesSUCCESS : (TER)tecPRECISION_LOSS)); env.close(); } } @@ -2076,12 +2079,15 @@ struct EscrowToken_test : public beast::unit_test::suite env(pay(gw, bob, USD(1))); env.close(); + bool const largeMantissa = features[featureSingleAssetVault] || + features[featureLendingProtocol]; + // alice cannot create escrow for 1/10 iou - precision loss env(escrow::create(alice, bob, USD(1)), escrow::condition(escrow::cb1), escrow::finish_time(env.now() + 1s), fee(baseFee * 150), - ter(tecPRECISION_LOSS)); + ter(largeMantissa ? (TER)tesSUCCESS : (TER)tecPRECISION_LOSS)); env.close(); auto const seq1 = env.seq(alice); @@ -3924,9 +3930,13 @@ public: { using namespace test::jtx; FeatureBitset const all{testable_amendments()}; - testIOUWithFeats(all); - testMPTWithFeats(all); - testMPTWithFeats(all - fixTokenEscrowV1); + for (FeatureBitset const& feats : + {all - featureSingleAssetVault - featureLendingProtocol, all}) + { + testIOUWithFeats(feats); + testMPTWithFeats(feats); + testMPTWithFeats(feats - fixTokenEscrowV1); + } } }; diff --git a/src/test/app/LendingHelpers_test.cpp b/src/test/app/LendingHelpers_test.cpp index 50efe0ebe3..55fffad6b0 100644 --- a/src/test/app/LendingHelpers_test.cpp +++ b/src/test/app/LendingHelpers_test.cpp @@ -104,7 +104,7 @@ class LendingHelpers_test : public beast::unit_test::suite .name = "Multiple payments remaining", .periodicRate = Number{5, -2}, .paymentsRemaining = 3, - .expectedPaymentFactor = Number{367208564631245, -15}, + .expectedPaymentFactor = Number{3672085646312450436, -19}, }, // from calc { .name = "Zero payments remaining", @@ -172,7 +172,7 @@ class LendingHelpers_test : public beast::unit_test::suite loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60), .paymentsRemaining = 3, .expectedPeriodicPayment = - Number{3895690663961231, -13}, // from calc + Number{389569066396123265, -15}, // from calc }, }; @@ -229,7 +229,8 @@ class LendingHelpers_test : public beast::unit_test::suite }, { .name = "Standard case", - .periodicPayment = Number{3895690663961231, -13}, // from calc + .periodicPayment = + Number{389569066396123265, -15}, // from calc .periodicRate = loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60), .paymentsRemaining = 3, @@ -426,7 +427,7 @@ class LendingHelpers_test : public beast::unit_test::suite NetClock::time_point{NetClock::duration{3'000}}, .nextPaymentDueDate = 2'000, .expectedLateInterest = - Number{3170979198376459, -17}, // from calc + Number{317097919837645865, -19}, // from calc }, }; @@ -519,7 +520,7 @@ class LendingHelpers_test : public beast::unit_test::suite .prevPaymentDate = 2'000, .paymentInterval = 30 * 24 * 60 * 60, .expectedAccruedInterest = - Number{1929012345679012, -17}, // from calc + Number{1929012345679012346, -20}, // from calc }, }; @@ -587,7 +588,7 @@ class LendingHelpers_test : public beast::unit_test::suite .startDate = 1'000, .closeInterestRate = TenthBips32{0}, .expectedFullPaymentInterest = - Number{1929012345679012, -17}, // from calc + Number{1929012345679012346, -20}, // from calc }, { .name = "Standard case", @@ -600,7 +601,7 @@ class LendingHelpers_test : public beast::unit_test::suite .startDate = 1'000, .closeInterestRate = TenthBips32{10'000}, .expectedFullPaymentInterest = - Number{1000192901234568, -13}, // from calc + Number{1000192901234567901, -16}, // from calc }, }; diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 93be28e9e9..769ed40321 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -752,30 +752,36 @@ class LoanBroker_test : public beast::unit_test::suite // LoanBrokerID env(set(alice, vault.vaultID), loanBrokerID(nextKeylet.key), - ter(tecNO_ENTRY)); + ter(tecNO_ENTRY), + THISLINE); // VaultID env(set(alice, nextKeylet.key), loanBrokerID(broker->key()), - ter(tecNO_PERMISSION)); + ter(tecNO_ENTRY), + THISLINE); // Owner env(set(evan, vault.vaultID), loanBrokerID(broker->key()), - ter(tecNO_PERMISSION)); + ter(tecNO_PERMISSION), + THISLINE); // ManagementFeeRate env(set(alice, vault.vaultID), loanBrokerID(broker->key()), managementFeeRate(maxManagementFeeRate), - ter(temINVALID)); + ter(temINVALID), + THISLINE); // CoverRateMinimum env(set(alice, vault.vaultID), loanBrokerID(broker->key()), coverRateMinimum(maxManagementFeeRate), - ter(temINVALID)); + ter(temINVALID), + THISLINE); // CoverRateLiquidation env(set(alice, vault.vaultID), loanBrokerID(broker->key()), coverRateLiquidation(maxManagementFeeRate), - ter(temINVALID)); + ter(temINVALID), + THISLINE); // fields that can be changed testData = "Test Data 1234"; @@ -783,23 +789,43 @@ class LoanBroker_test : public beast::unit_test::suite env(set(alice, vault.vaultID), loanBrokerID(broker->key()), data(std::string(maxDataPayloadLength + 1, 'W')), - ter(temINVALID)); + ter(temINVALID), + THISLINE); // Bad debt maximum env(set(alice, vault.vaultID), loanBrokerID(broker->key()), debtMaximum(Number(-175, -1)), - ter(temINVALID)); + ter(temINVALID), + THISLINE); + Number debtMax{175, -1}; + if (vault.asset.integral()) + { + env(set(alice, vault.vaultID), + loanBrokerID(broker->key()), + data(testData), + debtMaximum(debtMax), + ter(tecPRECISION_LOSS), + THISLINE); + roundToAsset(vault.asset, debtMax); + } // Data & Debt maximum env(set(alice, vault.vaultID), loanBrokerID(broker->key()), data(testData), - debtMaximum(Number(175, -1))); + debtMaximum(debtMax), + THISLINE); }, [&](SLE::const_ref broker) { // Check the updated fields BEAST_EXPECT(checkVL(broker->at(sfData), testData)); - BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(175, -1)); + Number const expected = + STAmount{vault.asset, Number(175, -1)}; + auto const actual = broker->at(sfDebtMaximum); + BEAST_EXPECTS( + actual == expected, + "Expected: " + to_string(expected) + + ", Actual: " + to_string(actual)); }); lifecycle( @@ -1457,9 +1483,14 @@ class LoanBroker_test : public beast::unit_test::suite env.close(); PrettyAsset const asset = [&]() { - env(trust(alice, issuer["IOU"](1'000'000)), THISLINE); + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); env.close(); - return PrettyAsset(issuer["IOU"]); + PrettyAsset const mptAsset = mptt["MPT"]; + mptt.authorize({.account = alice}); + env.close(); + return mptAsset; }(); env(pay(issuer, alice, asset(100'000)), THISLINE); @@ -1512,6 +1543,40 @@ class LoanBroker_test : public beast::unit_test::suite tx2[sfDebtMaximum] = 0; env(tx2, ter(tesSUCCESS), THISLINE); + + tx2[sfDebtMaximum] = Json::Value::maxInt; + env(tx2, ter(tesSUCCESS), THISLINE); + + { + auto const dm = power(2, 64) - 1; + BEAST_EXPECT(dm > maxMPTokenAmount); + tx2[sfDebtMaximum] = dm; + env(tx2, ter(temINVALID), THISLINE); + } + + { + auto const dm = power(2, 63) - 1; + BEAST_EXPECTS(dm > maxMPTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, ter(temINVALID), THISLINE); + } + + { + auto const dm = power(2, 63) - 3; + BEAST_EXPECTS(dm == maxMPTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, ter(tesSUCCESS), THISLINE); + } + + { + auto const dm = 2 * (power(2, 62) - 1) + 1; + BEAST_EXPECTS(dm == maxMPTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, ter(tesSUCCESS), THISLINE); + } + + tx2[sfDebtMaximum] = Number{9223372036854775807, 0}; + env(tx2, ter(tesSUCCESS), THISLINE); } void diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index e4f5360043..e9780211de 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -352,8 +352,14 @@ protected: env.balance(account, broker.asset) - (balanceBefore - balanceChangeAmount), borrowerScale); - env.test.BEAST_EXPECT( - roundToScale(difference, loanScale) >= beast::zero); + env.test.expect( + roundToScale(difference, loanScale) >= beast::zero, + "Balance before: " + to_string(balanceBefore.value()) + + ", expected change: " + to_string(balanceChangeAmount) + + ", difference (balance after - expected): " + + to_string(difference), + __FILE__, + __LINE__); } } @@ -2396,7 +2402,7 @@ protected: interval * Number(12, -2) / secondsInYear; BEAST_EXPECT( periodicRate == - Number(2283105022831050, -21, Number::unchecked{})); + Number(2283105022831050228ULL, -24, Number::normalized{})); STAmount const principalOutstanding{ broker.asset, state.principalOutstanding}; STAmount const accruedInterest{ @@ -2449,6 +2455,10 @@ protected: getCurrentState(env, broker, loanKeylet, verifyLoanStatus); env.close(); + BEAST_EXPECT( + STAmount(broker.asset, state.periodicPayment) == + broker.asset(Number(8333457002039338267, -17))); + // Make all the payments in one transaction // service fee is 2 auto const startingPayments = state.paymentRemaining; @@ -2456,15 +2466,28 @@ protected: NumberRoundModeGuard mg(Number::upward); auto const rawPayoff = startingPayments * (state.periodicPayment + broker.asset(2).value()); - STAmount const payoffAmount{broker.asset, rawPayoff}; + STAmount payoffAmount{broker.asset, rawPayoff}; BEAST_EXPECTS( payoffAmount == - broker.asset(Number(1024014840139457, -12)), + broker.asset(Number(1024014840244721, -12)), to_string(payoffAmount)); BEAST_EXPECT(payoffAmount > state.principalOutstanding); + + payoffAmount = roundToScale(payoffAmount, state.loanScale); + return payoffAmount; }(); + auto const totalPayoffValue = state.totalValue + + startingPayments * broker.asset(2).value(); + STAmount const totalPayoffAmount{ + broker.asset, totalPayoffValue}; + + BEAST_EXPECTS( + totalPayoffAmount == payoffAmount, + "Payoff amount: " + to_string(payoffAmount) + + ". Total Value: " + to_string(totalPayoffAmount)); + singlePayment( loanKeylet, verifyLoanStatus, @@ -2633,7 +2656,7 @@ protected: interval * Number(12, -2) / secondsInYear; BEAST_EXPECT( periodicRate == - Number(2283105022831050, -21, Number::unchecked{})); + Number(2283105022831050228, -24, Number::normalized{})); STAmount const roundedPeriodicPayment{ broker.asset, roundPeriodicPayment( @@ -2651,7 +2674,7 @@ protected: roundedPeriodicPayment == roundToScale( broker.asset( - Number(8333457001162141, -14), Number::upward), + Number(8333457002039338267, -17), Number::upward), state.loanScale, Number::upward)); // 83334570.01162141 @@ -2666,7 +2689,7 @@ protected: totalDue == roundToScale( broker.asset( - Number(8533457001162141, -14), Number::upward), + Number(8533457002039338267, -17), Number::upward), state.loanScale, Number::upward)); @@ -2702,7 +2725,7 @@ protected: transactionAmount == roundToScale( broker.asset( - Number(9533457001162141, -14), Number::upward), + Number(9533457002039400, -14), Number::upward), state.loanScale, Number::upward)); @@ -2735,9 +2758,15 @@ protected: state.paymentRemaining, broker.params.managementFeeRate); - BEAST_EXPECT( - paymentComponents.trackedValueDelta <= - roundedPeriodicPayment); + BEAST_EXPECTS( + paymentComponents.specialCase == + detail::PaymentSpecialCase::final || + paymentComponents.trackedValueDelta <= + roundedPeriodicPayment, + "Delta: " + + to_string(paymentComponents.trackedValueDelta) + + ", periodic payment: " + + to_string(roundedPeriodicPayment)); xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( @@ -2792,8 +2821,10 @@ protected: paymentComponents.trackedInterestPart() + paymentComponents.trackedManagementFeeDelta); BEAST_EXPECT( + paymentComponents.specialCase == + detail::PaymentSpecialCase::final || paymentComponents.trackedValueDelta <= - roundedPeriodicPayment); + roundedPeriodicPayment); BEAST_EXPECT( state.paymentRemaining < 12 || @@ -2804,7 +2835,7 @@ protected: Number::upward) == roundToScale( broker.asset( - Number(8333228695260180, -14), + Number(8333228691531218890, -17), Number::upward), state.loanScale, Number::upward)); @@ -3689,7 +3720,7 @@ protected: env(pay(issuer, borrower, mptAsset(10'000))); env.close(); - std::array const assets{xrpAsset, mptAsset, iouAsset}; + std::array const assets{iouAsset, xrpAsset, mptAsset}; // Create vaults and loan brokers std::vector brokers; @@ -5041,7 +5072,6 @@ protected: auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest{1, 3}; - auto const startDate = env.now() + 60s; auto createJson = env.json( set(borrower, broker.brokerID, principalRequest), @@ -5065,13 +5095,13 @@ protected: auto const keylet = keylet::loan(broker.brokerID, loanSequence); createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - env(createJson, ter(tecPRECISION_LOSS), THISLINE); - env.close(startDate); + env(createJson, THISLINE); + env.close(); auto loanPayTx = env.json( pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); loanPayTx["Amount"]["value"] = "0.000281284125490196"; - env(loanPayTx, ter(tecNO_ENTRY)); + env(loanPayTx, ter(tecINSUFFICIENT_PAYMENT), THISLINE); env.close(); } @@ -5640,21 +5670,26 @@ protected: BEAST_EXPECT(beforeState.periodicPayment > 0); // pay all but the last payment - Number const payment = beforeState.periodicPayment * (total - 1); - XRPAmount const payFee{ - baseFee * ((total - 1) / loanPaymentsPerFeeIncrement + 1)}; - auto loanPayTx = env.json( - pay(borrower, keylet.key, STAmount{broker.asset, payment}), - fee(payFee)); - env(loanPayTx, ter(tesSUCCESS)); - env.close(); + { + NumberRoundModeGuard mg{Number::upward}; + Number const payment = + beforeState.periodicPayment * (total - 1); + XRPAmount const payFee{ + baseFee * ((total - 1) / loanPaymentsPerFeeIncrement + 1)}; + STAmount const paymentAmount = roundToScale( + STAmount{broker.asset, payment}, beforeState.loanScale); + auto loanPayTx = env.json( + pay(borrower, keylet.key, paymentAmount), fee(payFee)); + env(loanPayTx, ter(tesSUCCESS)); + env.close(); + } // The loan is on the last payment auto const afterState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(afterState.paymentRemaining == 1); BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace); BEAST_EXPECT( afterState.previousPaymentDate == maxTime - grace - interval); - BEAST_EXPECT(afterState.paymentRemaining == 1); } } diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 747f78ef6b..65cb753755 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1594,7 +1594,7 @@ class MPToken_test : public beast::unit_test::suite jv[jss::secret] = alice.name(); jv[jss::tx_json] = pay(alice, bob, mpt); jv[jss::tx_json][jss::Amount][jss::value] = - to_string(maxMPTokenAmount + 1); + std::to_string(maxMPTokenAmount + 1); auto const jrr = env.rpc("json", "submit", to_string(jv)); BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); } @@ -2474,7 +2474,7 @@ class MPToken_test : public beast::unit_test::suite alice.name(), makeMptID(env.seq(alice), alice)); Json::Value jv = claw(alice, mpt(1), bob); - jv[jss::Amount][jss::value] = to_string(maxMPTokenAmount + 1); + jv[jss::Amount][jss::value] = std::to_string(maxMPTokenAmount + 1); Json::Value jv1; jv1[jss::secret] = alice.name(); jv1[jss::tx_json] = jv; diff --git a/src/test/basics/IOUAmount_test.cpp b/src/test/basics/IOUAmount_test.cpp index 305f2c83a1..d299f439d4 100644 --- a/src/test/basics/IOUAmount_test.cpp +++ b/src/test/basics/IOUAmount_test.cpp @@ -141,15 +141,28 @@ public: { testcase("IOU strings"); - BEAST_EXPECT(to_string(IOUAmount(-2, 0)) == "-2"); - BEAST_EXPECT(to_string(IOUAmount(0, 0)) == "0"); - BEAST_EXPECT(to_string(IOUAmount(2, 0)) == "2"); - BEAST_EXPECT(to_string(IOUAmount(25, -3)) == "0.025"); - BEAST_EXPECT(to_string(IOUAmount(-25, -3)) == "-0.025"); - BEAST_EXPECT(to_string(IOUAmount(25, 1)) == "250"); - BEAST_EXPECT(to_string(IOUAmount(-25, 1)) == "-250"); - BEAST_EXPECT(to_string(IOUAmount(2, 20)) == "2000000000000000e5"); - BEAST_EXPECT(to_string(IOUAmount(-2, -20)) == "-2000000000000000e-35"); + auto test = [this](IOUAmount const& n, std::string const& expected) { + auto const result = to_string(n); + std::stringstream ss; + ss << "to_string(" << result << "). Expected: " << expected; + BEAST_EXPECTS(result == expected, ss.str()); + }; + + for (auto const mantissaSize : + {MantissaRange::small, MantissaRange::large}) + { + NumberMantissaScaleGuard mg(mantissaSize); + + test(IOUAmount(-2, 0), "-2"); + test(IOUAmount(0, 0), "0"); + test(IOUAmount(2, 0), "2"); + test(IOUAmount(25, -3), "0.025"); + test(IOUAmount(-25, -3), "-0.025"); + test(IOUAmount(25, 1), "250"); + test(IOUAmount(-25, 1), "-250"); + test(IOUAmount(2, 20), "2e20"); + test(IOUAmount(-2, -20), "-2e-20"); + } } void diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index b7c5ee45b7..1fa5ae6e8f 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -14,46 +15,84 @@ public: void testZero() { - testcase("zero"); + testcase << "zero " << to_string(Number::getMantissaScale()); - Number const z{0, 0}; + for (Number const& z : {Number{0, 0}, Number{0}}) + { + BEAST_EXPECT(z.mantissa() == 0); + BEAST_EXPECT(z.exponent() == Number{}.exponent()); - 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); + BEAST_EXPECT((z + z) == z); + BEAST_EXPECT((z - z) == z); + BEAST_EXPECT(z == -z); + } } void test_limits() { - testcase("test_limits"); + auto const scale = Number::getMantissaScale(); + testcase << "test_limits " << to_string(scale); bool caught = false; + auto const minMantissa = Number::minMantissa(); try { - Number x{10'000'000'000'000'000, 32768}; + Number x = + Number{false, minMantissa * 10, 32768, Number::normalized{}}; } 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})); + + auto test = [this](auto const& x, auto const& y, int line) { + auto const result = x == y; + std::stringstream ss; + ss << x << " == " << y << " -> " << (result ? "true" : "false"); + expect(result, ss.str(), __FILE__, line); + }; + + test( + Number{false, minMantissa * 10, 32767, Number::normalized{}}, + Number{false, minMantissa, 32768, Number::normalized{}}, + __LINE__); + test( + Number{false, minMantissa, -32769, Number::normalized{}}, + Number{}, + __LINE__); + test( + Number{false, minMantissa, 32000, Number::normalized{}} * 1'000 + + Number{false, 1'500, 32000, Number::normalized{}}, + Number{false, minMantissa + 2, 32003, Number::normalized{}}, + __LINE__); + // 9,223,372,036,854,775,808 + + test( + Number{std::numeric_limits::min()}, + scale == MantissaRange::small + ? Number{-9'223'372'036'854'776, 3} + : Number{true, 9'223'372'036'854'775'808ULL, 0, Number::normalized{}}, + __LINE__); + test( + Number{std::numeric_limits::min() + 1}, + scale == MantissaRange::small ? Number{-9'223'372'036'854'776, 3} + : Number{-9'223'372'036'854'775'807}, + __LINE__); + test( + Number{std::numeric_limits::max()}, + Number{ + scale == MantissaRange::small + ? 9'223'372'036'854'776 + : std::numeric_limits::max(), + 18 - Number::mantissaLog()}, + __LINE__); caught = false; try { - Number q{99'999'999'999'999'999, 32767}; + [[maybe_unused]] + Number q = + Number{false, minMantissa, 32767, Number::normalized{}} * 100; } catch (std::overflow_error const&) { @@ -65,76 +104,307 @@ public: void test_add() { - testcase("test_add"); + auto const scale = Number::getMantissaScale(); + testcase << "test_add " << to_string(scale); + 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 + auto const cSmall = std::to_array( + {{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}, Number{}, 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}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items from C + // with larger mantissa + { + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'065'556, -18}}, + {Number{-1'000'000'000'000'000, -15}, + Number{-6'555'555'555'555'555, -29}, + Number{-1'000'000'000'000'065'556, -18}}, + {Number{-1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{ + true, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{-6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{ + false, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{}, Number{5}, Number{5}}, + {Number{5}, Number{}, Number{5}}, + {Number{5'555'555'555'555'555'000, -32768}, + Number{-5'555'555'555'555'554'000, -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}}, + // Items from cSmall expanded for the larger mantissa + {Number{1'000'000'000'000'000'000, -18}, + Number{6'555'555'555'555'555'555, -35}, + Number{1'000'000'000'000'000'066, -18}}, + {Number{-1'000'000'000'000'000'000, -18}, + Number{-6'555'555'555'555'555'555, -35}, + Number{-1'000'000'000'000'000'066, -18}}, + {Number{-1'000'000'000'000'000'000, -18}, + Number{6'555'555'555'555'555'555, -35}, + Number{ + true, + 9'999'999'999'999'999'344ULL, + -19, + Number::normalized{}}}, + {Number{-6'555'555'555'555'555'555, -35}, + Number{1'000'000'000'000'000'000, -18}, + Number{ + false, + 9'999'999'999'999'999'344ULL, + -19, + Number::normalized{}}}, + {Number{}, Number{5}, Number{5}}, + {Number{5'555'555'555'555'555'555, -32768}, + Number{-5'555'555'555'555'555'554, -32768}, + Number{0}}, + {Number{ + true, + 9'999'999'999'999'999'999ULL, + -37, + Number::normalized{}}, + Number{1'000'000'000'000'000'000, -18}, + Number{ + false, + 9'999'999'999'999'999'990ULL, + -19, + Number::normalized{}}}, + {Number{Number::maxRep}, + Number{6, -1}, + Number{Number::maxRep / 10, 1}}, + {Number{Number::maxRep - 1}, + Number{1, 0}, + Number{Number::maxRep}}, + // Test extremes + { + // Each Number operand rounds up, so the actual mantissa is + // minMantissa + Number{ + false, + 9'999'999'999'999'999'999ULL, + 0, + Number::normalized{}}, + Number{ + false, + 9'999'999'999'999'999'999ULL, + 0, + Number::normalized{}}, + Number{2, 19}, + }, + { + // Does not round. Mantissas are going to be > maxRep, so if + // added together as uint64_t's, the result will overflow. + // With addition using uint128_t, there's no problem. After + // normalizing, the resulting mantissa ends up less than + // maxRep. + Number{ + false, + 9'999'999'999'999'999'990ULL, + 0, + Number::normalized{}}, + Number{ + false, + 9'999'999'999'999'999'990ULL, + 0, + Number::normalized{}}, + Number{ + false, + 1'999'999'999'999'999'998ULL, + 1, + Number::normalized{}}, + }, + }); + auto test = [this](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = x + y; + std::stringstream ss; + ss << x << " + " << y << " = " << result << ". Expected: " << z; + BEAST_EXPECTS(result == z, ss.str()); + } + }; + if (scale == MantissaRange::small) + test(cSmall); + else + test(cLarge); { - Number{9'999'999'999'999'999, 32768} + - Number{5'000'000'000'000'000, 32767}; + bool caught = false; + try + { + Number{ + false, Number::maxMantissa(), 32768, Number::normalized{}} + + Number{ + false, + Number::minMantissa(), + 32767, + Number::normalized{}} * + 5; + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); } void test_sub() { - testcase("test_sub"); + auto const scale = Number::getMantissaScale(); + testcase << "test_sub " << to_string(scale); + 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); + auto const cSmall = std::to_array( + {{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}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items from C + // with larger mantissa + { + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{ + false, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{ + true, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {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}}, + // Items from cSmall expanded for the larger mantissa + {Number{1'000'000'000'000'000'000, -18}, + Number{6'555'555'555'555'555'555, -32}, + Number{ + false, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{6'555'555'555'555'555'555, -32}, + Number{1'000'000'000'000'000'000, -18}, + Number{ + true, + 9'999'999'999'999'344'444ULL, + -19, + Number::normalized{}}}, + {Number{1'000'000'000'000'000'000, -18}, + Number{1'000'000'000'000'000'000, -18}, + Number{0}}, + {Number{1'000'000'000'000'000'000, -18}, + Number{1'000'000'000'000'000'001, -18}, + Number{-1'000'000'000'000'000'000, -36}}, + {Number{1'000'000'000'000'000'001, -18}, + Number{1'000'000'000'000'000'000, -18}, + Number{1'000'000'000'000'000'000, -36}}, + {Number{Number::maxRep}, + Number{6, -1}, + Number{Number::maxRep - 1}}, + {Number{false, Number::maxRep + 1, 0, Number::normalized{}}, + Number{1, 0}, + Number{Number::maxRep / 10 + 1, 1}}, + {Number{false, Number::maxRep + 1, 0, Number::normalized{}}, + Number{3, 0}, + Number{Number::maxRep}}, + {power(2, 63), Number{3, 0}, Number{Number::maxRep}}, + }); + auto test = [this](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = x - y; + std::stringstream ss; + ss << x << " - " << y << " = " << result << ". Expected: " << z; + BEAST_EXPECTS(result == z, ss.str()); + } + }; + if (scale == MantissaRange::small) + test(cSmall); + else + test(cLarge); } void test_mul() { - testcase("test_mul"); + auto const scale = Number::getMantissaScale(); + testcase << "test_mul " << to_string(scale); + using Case = std::tuple; + auto test = [this](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = x * y; + std::stringstream ss; + ss << x << " * " << y << " = " << result << ". Expected: " << z; + BEAST_EXPECTS(result == z, ss.str()); + } + }; + auto tests = [&](auto const& cSmall, auto const& cLarge) { + if (scale == MantissaRange::small) + test(cSmall); + else + test(cLarge); + }; + auto const maxMantissa = Number::maxMantissa(); + saveNumberRoundMode save{Number::setround(Number::to_nearest)}; { - Case c[]{ + auto const cSmall = std::to_array({ {Number{7}, Number{8}, Number{56}}, {Number{1414213562373095, -15}, Number{1414213562373095, -15}, @@ -150,166 +420,520 @@ public: Number{1000000000000000, -14}}, {Number{1000000000000000, -32768}, Number{1000000000000000, -32768}, - Number{0}}}; - for (auto const& [x, y, z] : c) - BEAST_EXPECT(x * y == z); + Number{0}}, + // Maximum mantissa range + {Number{9'999'999'999'999'999, 0}, + Number{9'999'999'999'999'999, 0}, + Number{9'999'999'999'999'998, 16}}, + }); + auto const cLarge = std::to_array({ + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{ + false, + 9'999'999'999'999'999'579ULL, + -18, + Number::normalized{}}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2000000000000000001, -18}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999998, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{1999999999999999999, -18}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{10, 0}}, + // Maximum mantissa range - rounds up to 1e19 + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1, 38}}, + // Maximum int64 range + {Number{Number::maxRep, 0}, + Number{Number::maxRep, 0}, + Number{85'070'591'730'234'615'85, 19}}, + }); + tests(cSmall, cLarge); } Number::setround(Number::towards_zero); + testcase << "test_mul " << to_string(Number::getMantissaScale()) + << " 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); + auto const cSmall = std::to_array( + {{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}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + { + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{ + false, + 9999999999999999579ULL, + -18, + Number::normalized{}}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2, 0}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999997, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{1999999999999999999, -18}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{10, 0}}, + // Maximum mantissa range - rounds down to maxMantissa/10e1 + // 99'999'999'999'999'999'800'000'000'000'000'000'100 + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{false, maxMantissa, 0, Number::normalized{}}, + Number{ + false, + maxMantissa / 10 - 1, + 20, + Number::normalized{}}}, + // Maximum int64 range + // 85'070'591'730'234'615'847'396'907'784'232'501'249 + {Number{Number::maxRep, 0}, + Number{Number::maxRep, 0}, + Number{85'070'591'730'234'615'84, 19}}, + }); + tests(cSmall, cLarge); } Number::setround(Number::downward); + testcase << "test_mul " << to_string(Number::getMantissaScale()) + << " 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); + auto const cSmall = std::to_array( + {{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}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + { + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999861, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{ + false, + 9'999'999'999'999'999'579ULL, + -18, + Number::normalized{}}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2, 0}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999998, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{1999999999999999999, -18}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{10, 0}}, + // Maximum mantissa range - rounds down to maxMantissa/10e1 + // 99'999'999'999'999'999'800'000'000'000'000'000'100 + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{false, maxMantissa, 0, Number::normalized{}}, + Number{ + false, + maxMantissa / 10 - 1, + 20, + Number::normalized{}}}, + // Maximum int64 range + // 85'070'591'730'234'615'847'396'907'784'232'501'249 + {Number{Number::maxRep, 0}, + Number{Number::maxRep, 0}, + Number{85'070'591'730'234'615'84, 19}}, + }); + tests(cSmall, cLarge); } Number::setround(Number::upward); + testcase << "test_mul " << to_string(Number::getMantissaScale()) + << " 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); + auto const cSmall = std::to_array( + {{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}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + { + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-1999999999999999861, -18}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{1999999999999999862, -18}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{999999999999999958, -17}}, + {Number{1000000000000000000, -32768}, + Number{1000000000000000000, -32768}, + Number{0}}, + // Items from cSmall expanded for the larger mantissa, + // except duplicates. Sadly, it looks like sqrt(2)^2 != 2 + // with higher precision + {Number{1414213562373095049, -18}, + Number{1414213562373095049, -18}, + Number{2000000000000000001, -18}}, + {Number{-1414213562373095048, -18}, + Number{1414213562373095048, -18}, + Number{-1999999999999999997, -18}}, + {Number{-1414213562373095048, -18}, + Number{-1414213562373095049, -18}, + Number{2, 0}}, + {Number{3214285714285714278, -18}, + Number{3111111111111111119, -18}, + Number{1000000000000000001, -17}}, + // Maximum mantissa range - rounds up to minMantissa*10 + // 1e19*1e19=1e38 + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1, 38}}, + // Maximum int64 range + // 85'070'591'730'234'615'847'396'907'784'232'501'249 + {Number{Number::maxRep, 0}, + Number{Number::maxRep, 0}, + Number{85'070'591'730'234'615'85, 19}}, + }); + tests(cSmall, cLarge); } - bool caught = false; - try + testcase << "test_mul " << to_string(Number::getMantissaScale()) + << " overflow"; { - Number{9'999'999'999'999'999, 32768} * - Number{5'000'000'000'000'000, 32767}; + bool caught = false; + try + { + Number{false, maxMantissa, 32768, Number::normalized{}} * + Number{ + false, + Number::minMantissa() * 5, + 32767, + Number::normalized{}}; + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); } - catch (std::overflow_error const&) - { - caught = true; - } - BEAST_EXPECT(caught); } void test_div() { - testcase("test_div"); + auto const scale = Number::getMantissaScale(); + testcase << "test_div " << to_string(scale); + using Case = std::tuple; + auto test = [this](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = x / y; + std::stringstream ss; + ss << x << " / " << y << " = " << result << ". Expected: " << z; + BEAST_EXPECTS(result == z, ss.str()); + } + }; + auto const maxMantissa = Number::maxMantissa(); + auto tests = [&](auto const& cSmall, auto const& cLarge) { + if (scale == MantissaRange::small) + test(cSmall); + else + test(cLarge); + }; 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); + auto const cSmall = std::to_array( + {{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}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{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'667, -19}}, + {Number{-2}, + Number{3}, + Number{-6'666'666'666'666'666'667, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, + Number{1414213562373095049, -13}, + Number{1}}, + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::normalized{}}}}); + tests(cSmall, cLarge); } + testcase << "test_div " << to_string(Number::getMantissaScale()) + << " towards_zero"; 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); + auto const cSmall = std::to_array( + {{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}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{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'666, -19}}, + {Number{-2}, + Number{3}, + Number{-6'666'666'666'666'666'666, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, + Number{1414213562373095049, -13}, + Number{1}}, + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::normalized{}}}}); + tests(cSmall, cLarge); } + testcase << "test_div " << to_string(Number::getMantissaScale()) + << " downward"; 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); + auto const cSmall = std::to_array( + {{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}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{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'666, -19}}, + {Number{-2}, + Number{3}, + Number{-6'666'666'666'666'666'667, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'571, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, + Number{1414213562373095049, -13}, + Number{1}}, + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::normalized{}}}}); + tests(cSmall, cLarge); } + testcase << "test_div " << to_string(Number::getMantissaScale()) + << " upward"; 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); + auto const cSmall = std::to_array( + {{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}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'429, -16}}}); + auto const cLarge = std::to_array( + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items + // from C with larger mantissa + {{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'667, -19}}, + {Number{-2}, + Number{3}, + Number{-6'666'666'666'666'666'666, -19}}, + {Number{1}, Number{7}, Number{1'428'571'428'571'428'572, -19}}, + // Items from cSmall expanded for the larger mantissa, except + // duplicates. + {Number{1414213562373095049, -13}, + Number{1414213562373095049, -13}, + Number{1}}, + {Number{false, maxMantissa, 0, Number::normalized{}}, + Number{1'000'000'000'000'000'000}, + Number{false, maxMantissa, -18, Number::normalized{}}}}); + tests(cSmall, cLarge); } + testcase << "test_div " << to_string(Number::getMantissaScale()) + << " overflow"; bool caught = false; try { @@ -325,20 +949,59 @@ public: void test_root() { - testcase("test_root"); + auto const scale = Number::getMantissaScale(); + testcase << "test_root " << to_string(scale); + 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)); + auto test = [this](auto const& c) { + for (auto const& [x, y, z] : c) + { + auto const result = root(x, y); + std::stringstream ss; + ss << "root(" << x << ", " << y << ") = " << result + << ". Expected: " << z; + BEAST_EXPECTS(result == z, ss.str()); + } + }; + /* + auto tests = [&](auto const& cSmall, auto const& cLarge) { + test(cSmall); + if (scale != MantissaRange::small) + test(cLarge); + }; + */ + + auto const cSmall = std::to_array( + {{Number{2}, 2, Number{1414213562373095049, -18}}, + {Number{2'000'000}, 2, Number{1414213562373095049, -15}}, + {Number{2, -30}, 2, Number{1414213562373095049, -33}}, + {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}}}); + auto const cLarge = std::to_array({ + {Number{false, Number::maxMantissa() - 9, -1, Number::normalized{}}, + 2, + Number{false, 999'999'999'999'999'999, -9, Number::normalized{}}}, + {Number{false, Number::maxMantissa() - 9, 0, Number::normalized{}}, + 2, + Number{ + false, 3'162'277'660'168'379'330, -9, Number::normalized{}}}, + {Number{Number::maxRep}, + 2, + Number{false, 3'037'000'499'976049692, -9, Number::normalized{}}}, + {Number{Number::maxRep}, + 4, + Number{false, 55'108'98747006743627, -14, Number::normalized{}}}, + }); + test(cSmall); + if (Number::getMantissaScale() != MantissaRange::small) + { + NumberRoundModeGuard mg(Number::towards_zero); + test(cLarge); + } bool caught = false; try { @@ -361,10 +1024,52 @@ public: BEAST_EXPECT(caught); } + void + test_root2() + { + auto const scale = Number::getMantissaScale(); + testcase << "test_root2 " << to_string(scale); + + auto test = [this](auto const& c) { + for (auto const& x : c) + { + auto const expected = root(x, 2); + auto const result = root2(x); + std::stringstream ss; + ss << "root2(" << x << ") = " << result + << ". Expected: " << expected; + BEAST_EXPECTS(result == expected, ss.str()); + } + }; + + auto const cSmall = std::to_array({ + Number{2}, + Number{2'000'000}, + Number{2, -30}, + Number{27}, + Number{1}, + Number{5, -1}, + Number{0}, + Number{5625, -4}, + Number{Number::maxRep}, + }); + test(cSmall); + bool caught = false; + try + { + (void)root2(Number{-2}); + } + catch (std::overflow_error const&) + { + caught = true; + } + BEAST_EXPECT(caught); + } + void test_power1() { - testcase("test_power1"); + testcase << "test_power1 " << to_string(Number::getMantissaScale()); using Case = std::tuple; Case c[]{ {Number{64}, 0, Number{1}}, @@ -372,7 +1077,13 @@ public: {Number{64}, 2, Number{4096}}, {Number{-64}, 2, Number{4096}}, {Number{64}, 3, Number{262144}}, - {Number{-64}, 3, Number{-262144}}}; + {Number{-64}, 3, Number{-262144}}, + {Number{64}, + 11, + Number{false, 7378697629483820646ULL, 1, Number::normalized{}}}, + {Number{-64}, + 11, + Number{true, 7378697629483820646ULL, 1, Number::normalized{}}}}; for (auto const& [x, y, z] : c) BEAST_EXPECT((power(x, y) == z)); } @@ -380,7 +1091,7 @@ public: void test_power2() { - testcase("test_power2"); + testcase << "test_power2 " << to_string(Number::getMantissaScale()); using Case = std::tuple; Case c[]{ {Number{1}, 3, 7, Number{1}}, @@ -426,7 +1137,7 @@ public: void testConversions() { - testcase("testConversions"); + testcase << "testConversions " << to_string(Number::getMantissaScale()); IOUAmount x{5, 6}; Number y = x; @@ -452,7 +1163,7 @@ public: void test_to_integer() { - testcase("test_to_integer"); + testcase << "test_to_integer " << to_string(Number::getMantissaScale()); using Case = std::tuple; saveNumberRoundMode save{Number::setround(Number::to_nearest)}; { @@ -620,7 +1331,7 @@ public: void test_squelch() { - testcase("test_squelch"); + testcase << "test_squelch " << to_string(Number::getMantissaScale()); Number limit{1, -6}; BEAST_EXPECT((squelch(Number{2, -6}, limit) == Number{2, -6})); BEAST_EXPECT((squelch(Number{1, -6}, limit) == Number{1, -6})); @@ -633,22 +1344,129 @@ public: 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"); + auto const scale = Number::getMantissaScale(); + testcase << "testToString " << to_string(scale); + + auto test = [this](Number const& n, std::string const& expected) { + auto const result = to_string(n); + std::stringstream ss; + ss << "to_string(" << result << "). Expected: " << expected; + BEAST_EXPECTS(result == expected, ss.str()); + }; + + test(Number(-2, 0), "-2"); + test(Number(0, 0), "0"); + test(Number(2, 0), "2"); + test(Number(25, -3), "0.025"); + test(Number(-25, -3), "-0.025"); + test(Number(25, 1), "250"); + test(Number(-25, 1), "-250"); + test(Number(2, 20), "2e20"); + test(Number(-2, -20), "-2e-20"); + // Test the edges + // ((exponent < -(25)) || (exponent > -(5))))) + // or ((exponent < -(28)) || (exponent > -(8))))) + test(Number(2, -10), "0.0000000002"); + test(Number(2, -11), "2e-11"); + + test(Number(-2, 10), "-20000000000"); + test(Number(-2, 11), "-2e11"); + + switch (scale) + { + case MantissaRange::small: + + test(Number::min(), "1e-32753"); + test(Number::max(), "9999999999999999e32768"); + test(Number::lowest(), "-9999999999999999e32768"); + { + NumberRoundModeGuard mg(Number::towards_zero); + + auto const maxMantissa = Number::maxMantissa(); + BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999); + test( + Number{ + false, + maxMantissa * 1000 + 999, + -3, + Number::normalized()}, + "9999999999999999"); + test( + Number{ + true, + maxMantissa * 1000 + 999, + -3, + Number::normalized()}, + "-9999999999999999"); + + test( + Number{std::numeric_limits::max(), -3}, + "9223372036854775"); + test( + -(Number{std::numeric_limits::max(), -3}), + "-9223372036854775"); + + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775e3"); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775e3"); + } + break; + case MantissaRange::large: + // Test the edges + // ((exponent < -(28)) || (exponent > -(8))))) + test(Number::min(), "1e-32750"); + test(Number::max(), "9223372036854775807e32768"); + test(Number::lowest(), "-9223372036854775807e32768"); + { + NumberRoundModeGuard mg(Number::towards_zero); + + auto const maxMantissa = Number::maxMantissa(); + BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999'999ULL); + test( + Number{false, maxMantissa, 0, Number::normalized{}}, + "9999999999999999990"); + test( + Number{true, maxMantissa, 0, Number::normalized{}}, + "-9999999999999999990"); + + test( + Number{std::numeric_limits::max(), 0}, + "9223372036854775807"); + test( + -(Number{std::numeric_limits::max(), 0}), + "-9223372036854775807"); + + // Because the absolute value of min is larger than max, it + // will be scaled down to fit under max. Since we're + // rounding towards zero, the 8 at the end is dropped. + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775800"); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775800"); + } + + test( + Number{std::numeric_limits::max(), 0} + 1, + "9223372036854775810"); + test( + -(Number{std::numeric_limits::max(), 0} + 1), + "-9223372036854775810"); + break; + default: + BEAST_EXPECT(false); + } } void test_relationals() { - testcase("test_relationals"); + testcase << "test_relationals " + << to_string(Number::getMantissaScale()); BEAST_EXPECT(!(Number{100} < Number{10})); BEAST_EXPECT(Number{100} > Number{10}); BEAST_EXPECT(Number{100} >= Number{10}); @@ -658,7 +1476,7 @@ public: void test_stream() { - testcase("test_stream"); + testcase << "test_stream " << to_string(Number::getMantissaScale()); Number x{100}; std::ostringstream os; os << x; @@ -668,7 +1486,7 @@ public: void test_inc_dec() { - testcase("test_inc_dec"); + testcase << "test_inc_dec " << to_string(Number::getMantissaScale()); Number x{100}; Number y = +x; BEAST_EXPECT(x == y); @@ -685,19 +1503,19 @@ public: 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()}; + auto res2 = STAmount{issue, n}; BEAST_EXPECT(res2 == STAmount{7518784}); Number::setround(Number::towards_zero); - res2 = STAmount{issue, n.mantissa(), n.exponent()}; + res2 = STAmount{issue, n}; BEAST_EXPECT(res2 == STAmount{7518783}); Number::setround(Number::downward); - res2 = STAmount{issue, n.mantissa(), n.exponent()}; + res2 = STAmount{issue, n}; BEAST_EXPECT(res2 == STAmount{7518783}); Number::setround(Number::upward); - res2 = STAmount{issue, n.mantissa(), n.exponent()}; + res2 = STAmount{issue, n}; BEAST_EXPECT(res2 == STAmount{7518784}); } @@ -834,28 +1652,94 @@ public: } } + void + testInt64() + { + auto const scale = Number::getMantissaScale(); + testcase << "std::int64_t " << to_string(scale); + + // Control case + BEAST_EXPECT(Number::maxMantissa() > 10); + Number ten{10}; + BEAST_EXPECT(ten.exponent() <= 0); + + if (scale == MantissaRange::small) + { + BEAST_EXPECT( + std::numeric_limits::max() > INITIAL_XRP.drops()); + BEAST_EXPECT(Number::maxMantissa() < INITIAL_XRP.drops()); + Number const initalXrp{INITIAL_XRP}; + BEAST_EXPECT(initalXrp.exponent() > 0); + + Number const maxInt64{Number::maxRep}; + BEAST_EXPECT(maxInt64.exponent() > 0); + // 85'070'591'730'234'615'865'843'651'857'942'052'864 - 38 digits + BEAST_EXPECT( + (power(maxInt64, 2) == Number{85'070'591'730'234'62, 22})); + + Number const max = + Number{false, Number::maxMantissa(), 0, Number::normalized{}}; + BEAST_EXPECT(max.exponent() <= 0); + // 99'999'999'999'999'980'000'000'000'000'001 - 32 digits + BEAST_EXPECT((power(max, 2) == Number{99'999'999'999'999'98, 16})); + } + else + { + BEAST_EXPECT( + std::numeric_limits::max() > INITIAL_XRP.drops()); + BEAST_EXPECT(Number::maxMantissa() > INITIAL_XRP.drops()); + Number const initalXrp{INITIAL_XRP}; + BEAST_EXPECT(initalXrp.exponent() <= 0); + + Number const maxInt64{Number::maxRep}; + BEAST_EXPECT(maxInt64.exponent() <= 0); + // 85'070'591'730'234'615'847'396'907'784'232'501'249 - 38 digits + BEAST_EXPECT( + (power(maxInt64, 2) == Number{85'070'591'730'234'615'85, 19})); + + NumberRoundModeGuard mg(Number::towards_zero); + + auto const maxMantissa = Number::maxMantissa(); + Number const max = + Number{false, maxMantissa, 0, Number::normalized{}}; + BEAST_EXPECT(max.mantissa() == maxMantissa / 10); + BEAST_EXPECT(max.exponent() == 1); + // 99'999'999'999'999'999'800'000'000'000'000'000'100 - also 38 + // digits + BEAST_EXPECT(( + power(max, 2) == + Number{false, maxMantissa / 10 - 1, 20, Number::normalized{}})); + } + } + 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(); - test_truncate(); - testRounding(); + for (auto const scale : {MantissaRange::small, MantissaRange::large}) + { + NumberMantissaScaleGuard sg(scale); + testZero(); + test_limits(); + testToString(); + test_add(); + test_sub(); + test_mul(); + test_div(); + test_root(); + test_root2(); + test_power1(); + test_power2(); + testConversions(); + test_to_integer(); + test_squelch(); + test_relationals(); + test_stream(); + test_inc_dec(); + test_toSTAmount(); + test_truncate(); + testRounding(); + testInt64(); + } } }; diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h index 83366d61e2..208e3c4e5f 100644 --- a/src/test/jtx/AMMTest.h +++ b/src/test/jtx/AMMTest.h @@ -21,7 +21,12 @@ struct TestAMMArg std::optional> pool = std::nullopt; std::uint16_t tfee = 0; std::optional ter = std::nullopt; - std::vector features = {testable_amendments()}; + std::vector features = { + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + jtx::testable_amendments() - featureSingleAssetVault - + featureLendingProtocol}; + bool noLog = false; }; @@ -66,6 +71,15 @@ protected: public: AMMTestBase(); + static FeatureBitset + testable_amendments() + { + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + return jtx::testable_amendments() - featureSingleAssetVault - + featureLendingProtocol; + } + protected: /** testAMM() funds 30,000XRP and 30,000IOU * for each non-XRP asset to Alice and Carol diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index a310fc5b44..147307f7b7 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -261,6 +261,12 @@ struct XRP_t return xrpIssue(); } + bool + integral() const + { + return true; + } + /** Returns an amount of XRP as PrettyAmount, which is trivially convertible to STAmount @@ -400,6 +406,11 @@ public: { return issue(); } + bool + integral() const + { + return issue().integral(); + } /** Implicit conversion to Issue or Asset. @@ -490,6 +501,11 @@ public: { return mptIssue(); } + bool + integral() const + { + return true; + } /** Implicit conversion to MPTIssue or asset. diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index de7ce5504b..139b9113b9 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -105,9 +105,14 @@ AMMTestBase::testAMM( for (auto const& features : arg.features) { + // Use small Number mantissas for the life of this test. + NumberMantissaScaleGuard const sg{xrpl::MantissaRange::small}; + + // For now, just disable SAV entirely, which locks in the small Number + // mantissas Env env{ *this, - features, + features - featureSingleAssetVault - featureLendingProtocol, arg.noLog ? std::make_unique(&logs) : nullptr}; auto const [asset1, asset2] = diff --git a/src/test/protocol/STNumber_test.cpp b/src/test/protocol/STNumber_test.cpp index 1275c756cf..4e7a8388ee 100644 --- a/src/test/protocol/STNumber_test.cpp +++ b/src/test/protocol/STNumber_test.cpp @@ -29,10 +29,8 @@ struct STNumber_test : public beast::unit_test::suite } void - run() override + doRun() { - static_assert(!std::is_convertible_v); - { STNumber const stnum{sfNumber}; BEAST_EXPECT(stnum.getSType() == STI_NUMBER); @@ -127,6 +125,41 @@ struct STNumber_test : public beast::unit_test::suite BEAST_EXPECT( numberFromJson(sfNumber, "-0.000e6") == STNumber(sfNumber, 0)); + { + NumberRoundModeGuard mg(Number::towards_zero); + // maxint64 9,223,372,036,854,775,807 + auto const maxInt = + std::to_string(std::numeric_limits::max()); + // minint64 -9,223,372,036,854,775,808 + auto const minInt = + std::to_string(std::numeric_limits::min()); + if (Number::getMantissaScale() == MantissaRange::small) + { + BEAST_EXPECT( + numberFromJson(sfNumber, maxInt) == + STNumber(sfNumber, Number{9'223'372'036'854'775, 3})); + BEAST_EXPECT( + numberFromJson(sfNumber, minInt) == + STNumber(sfNumber, Number{-9'223'372'036'854'775, 3})); + } + else + { + BEAST_EXPECT( + numberFromJson(sfNumber, maxInt) == + STNumber( + sfNumber, Number{9'223'372'036'854'775'807, 0})); + BEAST_EXPECT( + numberFromJson(sfNumber, minInt) == + STNumber( + sfNumber, + Number{ + true, + 9'223'372'036'854'775'808ULL, + 0, + Number::normalized{}})); + } + } + constexpr auto imin = std::numeric_limits::min(); BEAST_EXPECT( numberFromJson(sfNumber, imin) == @@ -279,15 +312,21 @@ struct STNumber_test : public beast::unit_test::suite } } } + + void + run() override + { + static_assert(!std::is_convertible_v); + + for (auto const scale : {MantissaRange::small, MantissaRange::large}) + { + NumberMantissaScaleGuard sg(scale); + testcase << to_string(Number::getMantissaScale()); + doRun(); + } + } }; BEAST_DEFINE_TESTSUITE(STNumber, protocol, xrpl); -void -testCompile(std::ostream& out) -{ - STNumber number{sfNumber, 42}; - out << number; -} - } // namespace xrpl diff --git a/src/test/rpc/GetAggregatePrice_test.cpp b/src/test/rpc/GetAggregatePrice_test.cpp index 52f82ffc6c..0ffefc6cb6 100644 --- a/src/test/rpc/GetAggregatePrice_test.cpp +++ b/src/test/rpc/GetAggregatePrice_test.cpp @@ -191,18 +191,38 @@ public: // Aggregate data set includes all price oracle instances, no trimming // or time threshold { - Env env(*this); - OraclesData oracles; - prep(env, oracles); - // entire and trimmed stats - auto ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles); - BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.45"); - BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 10); - BEAST_EXPECT( - ret[jss::entire_set][jss::standard_deviation] == - "0.3027650354097492"); - BEAST_EXPECT(ret[jss::median] == "74.45"); - BEAST_EXPECT(ret[jss::time] == 946694900); + auto const all = testable_amendments(); + for (auto const& feats : + {all - featureSingleAssetVault - featureLendingProtocol, all}) + { + for (auto const mantissaSize : + {MantissaRange::small, MantissaRange::large}) + { + // Regardless of the features enabled, RPC is controlled by + // the global mantissa size. And since it's a thread-local, + // overriding it locally won't make a difference either. + // This will mean all RPC will use the default of "large". + NumberMantissaScaleGuard mg(mantissaSize); + + Env env(*this, feats); + OraclesData oracles; + prep(env, oracles); + // entire and trimmed stats + auto ret = + Oracle::aggregatePrice(env, "XRP", "USD", oracles); + BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.45"); + BEAST_EXPECT( + ret[jss::entire_set][jss::size].asUInt() == 10); + // Short: 0.3027650354097492 + BEAST_EXPECTS( + ret[jss::entire_set][jss::standard_deviation] == + "0.3027650354097491666", + ret[jss::entire_set][jss::standard_deviation] + .asString()); + BEAST_EXPECT(ret[jss::median] == "74.45"); + BEAST_EXPECT(ret[jss::time] == 946694900); + } + } } // Aggregate data set includes all price oracle instances @@ -215,15 +235,19 @@ public: Oracle::aggregatePrice(env, "XRP", "USD", oracles, 20, 100); BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.45"); BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 10); - BEAST_EXPECT( + // Short: "0.3027650354097492", + BEAST_EXPECTS( ret[jss::entire_set][jss::standard_deviation] == - "0.3027650354097492"); + "0.3027650354097491666", + ret[jss::entire_set][jss::standard_deviation].asString()); BEAST_EXPECT(ret[jss::median] == "74.45"); BEAST_EXPECT(ret[jss::trimmed_set][jss::mean] == "74.45"); BEAST_EXPECT(ret[jss::trimmed_set][jss::size].asUInt() == 6); - BEAST_EXPECT( + // Short: "0.187082869338697", + BEAST_EXPECTS( ret[jss::trimmed_set][jss::standard_deviation] == - "0.187082869338697"); + "0.1870828693386970693", + ret[jss::trimmed_set][jss::standard_deviation].asString()); BEAST_EXPECT(ret[jss::time] == 946694900); } @@ -274,15 +298,19 @@ public: Oracle::aggregatePrice(env, "XRP", "USD", oracles, 20, "200"); BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.6"); BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 7); - BEAST_EXPECT( + // Short: 0.2160246899469287 + BEAST_EXPECTS( ret[jss::entire_set][jss::standard_deviation] == - "0.2160246899469287"); + "0.2160246899469286744", + ret[jss::entire_set][jss::standard_deviation].asString()); BEAST_EXPECT(ret[jss::median] == "74.6"); BEAST_EXPECT(ret[jss::trimmed_set][jss::mean] == "74.6"); BEAST_EXPECT(ret[jss::trimmed_set][jss::size].asUInt() == 5); - BEAST_EXPECT( + // Short: 0.158113883008419 + BEAST_EXPECTS( ret[jss::trimmed_set][jss::standard_deviation] == - "0.158113883008419"); + "0.1581138830084189666", + ret[jss::trimmed_set][jss::standard_deviation].asString()); BEAST_EXPECT(ret[jss::time] == 946694900); } diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index 1cc6b1f5ae..c61145dda1 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -2,6 +2,8 @@ // #include +#include + namespace xrpl { bool @@ -338,6 +340,8 @@ LoanBrokerCoverClawback::doApply() sleBroker->at(sfCoverAvailable) -= clawAmount; view().update(sleBroker); + associateAsset(*sleBroker, vaultAsset); + // Transfer assets from pseudo-account to depositor. return accountSend( view(), brokerPseudoID, account, clawAmount, j_, WaiveTransferFee::Yes); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index b68cf46a00..ed47d40631 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -2,6 +2,8 @@ // #include +#include + namespace xrpl { bool @@ -100,6 +102,12 @@ LoanBrokerCoverDeposit::doApply() if (!broker) return tecINTERNAL; // LCOV_EXCL_LINE + auto const vault = view().read(keylet::vault(broker->at(sfVaultID))); + if (!vault) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const vaultAsset = vault->at(sfAsset); + auto const brokerPseudoID = broker->at(sfAccount); // Transfer assets from depositor to pseudo-account. @@ -116,6 +124,8 @@ LoanBrokerCoverDeposit::doApply() broker->at(sfCoverAvailable) += amount; view().update(broker); + associateAsset(*broker, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 830f9e26c1..5d4d2053ed 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -4,6 +4,7 @@ #include #include +#include namespace xrpl { @@ -156,12 +157,20 @@ LoanBrokerCoverWithdraw::doApply() if (!broker) return tecINTERNAL; // LCOV_EXCL_LINE + auto const vault = view().read(keylet::vault(broker->at(sfVaultID))); + if (!vault) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const vaultAsset = vault->at(sfAsset); + auto const brokerPseudoID = *broker->at(sfAccount); // Decrease the LoanBroker's CoverAvailable by Amount broker->at(sfCoverAvailable) -= amount; view().update(broker); + associateAsset(*broker, vaultAsset); + return doWithdraw( view(), tx, diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index 227bad10a9..f3f57f8659 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -2,6 +2,8 @@ // #include +#include + namespace xrpl { bool @@ -185,6 +187,8 @@ LoanBrokerDelete::doApply() adjustOwnerCount(view(), owner, -2, j_); } + associateAsset(*broker, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 0cbae4d779..b7e9e4c79f 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -2,6 +2,8 @@ // #include +#include + namespace xrpl { bool @@ -62,6 +64,15 @@ LoanBrokerSet::preflight(PreflightContext const& ctx) return tesSUCCESS; } +std::vector> const& +LoanBrokerSet::getValueFields() +{ + static std::vector> const valueFields{ + ~sfDebtMaximum}; + + return valueFields; +} + TER LoanBrokerSet::preclaim(PreclaimContext const& ctx) { @@ -70,8 +81,24 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx) auto const account = tx[sfAccount]; auto const vaultID = tx[sfVaultID]; + auto const sleVault = ctx.view.read(keylet::vault(vaultID)); + if (!sleVault) + { + JLOG(ctx.j.warn()) << "Vault does not exist."; + return tecNO_ENTRY; + } + Asset const asset = sleVault->at(sfAsset); + + if (account != sleVault->at(sfOwner)) + { + JLOG(ctx.j.warn()) << "Account is not the owner of the Vault."; + return tecNO_PERMISSION; + } + if (auto const brokerID = tx[~sfLoanBrokerID]) { + // Updating an existing Broker + auto const sleBroker = ctx.view.read(keylet::loanbroker(*brokerID)); if (!sleBroker) { @@ -104,18 +131,7 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx) } else { - auto const sleVault = ctx.view.read(keylet::vault(vaultID)); - if (!sleVault) - { - JLOG(ctx.j.warn()) << "Vault does not exist."; - return tecNO_ENTRY; - } - if (account != sleVault->at(sfOwner)) - { - JLOG(ctx.j.warn()) << "Account is not the owner of the Vault."; - return tecNO_PERMISSION; - } - if (auto const ter = canAddHolding(ctx.view, sleVault->at(sfAsset))) + if (auto const ter = canAddHolding(ctx.view, asset)) return ter; if (auto const ter = checkFrozen( @@ -125,6 +141,21 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx) return ter; } } + + // Check that relevant values can be represented as the vault asset + // type. This is mostly only relevant for integral (non-IOU) types + for (auto const& field : getValueFields()) + { + if (auto const value = tx[field]; + value && STAmount{asset, *value} != *value) + { + JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value + << ") can not be represented as a(n) " + << to_string(asset) << "."; + return tecPRECISION_LOSS; + } + } + return tesSUCCESS; } @@ -147,12 +178,20 @@ LoanBrokerSet::doApply() // LCOV_EXCL_STOP } + auto const vault = view.read(keylet::vault(broker->at(sfVaultID))); + if (!vault) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const vaultAsset = vault->at(sfAsset); + if (auto const data = tx[~sfData]) broker->at(sfData) = *data; if (auto const debtMax = tx[~sfDebtMaximum]) broker->at(sfDebtMaximum) = *debtMax; view.update(broker); + + associateAsset(*broker, vaultAsset); } else { @@ -168,6 +207,7 @@ LoanBrokerSet::doApply() // LCOV_EXCL_STOP } auto const vaultPseudoID = sleVault->at(sfAccount); + auto const vaultAsset = sleVault->at(sfAsset); auto const sequence = tx.getSeqValue(); auto owner = view.peek(keylet::account(account_)); @@ -224,6 +264,8 @@ LoanBrokerSet::doApply() broker->at(sfCoverRateLiquidation) = *coverLiq; view.insert(broker); + + associateAsset(*broker, vaultAsset); } return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.h b/src/xrpld/app/tx/detail/LoanBrokerSet.h index 625c0adeb2..57170b9cb9 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.h +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.h @@ -20,6 +20,9 @@ public: static NotTEC preflight(PreflightContext const& ctx); + static std::vector> const& + getValueFields(); + static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index 3643e6331b..ddb286db12 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -2,6 +2,8 @@ // #include +#include + namespace xrpl { bool @@ -78,9 +80,10 @@ LoanDelete::doApply() return tefBAD_LEDGER; // LCOV_EXCL_LINE auto const brokerPseudoAccount = brokerSle->at(sfAccount); - auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); + auto const vaultSle = view.peek(keylet::vault(brokerSle->at(sfVaultID))); if (!vaultSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const vaultAsset = vaultSle->at(sfAsset); // Remove LoanID from Directory of the LoanBroker pseudo-account. if (!view.dirRemove( @@ -125,6 +128,11 @@ LoanDelete::doApply() // Decrement the borrower's owner count adjustOwnerCount(view, borrowerSle, -1, j_); + // These associations shouldn't do anything, but do them just to be safe + associateAsset(*loanSle, vaultAsset); + associateAsset(*brokerSle, vaultAsset); + associateAsset(*vaultSle, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index bd0539ae4e..17dee8ffe8 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -2,6 +2,7 @@ // #include +#include #include namespace xrpl { @@ -412,7 +413,7 @@ LoanManage::doApply() if (!brokerSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); + auto const vaultSle = view.peek(keylet::vault(brokerSle->at(sfVaultID))); if (!vaultSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE auto const vaultAsset = vaultSle->at(sfAsset); @@ -426,6 +427,11 @@ LoanManage::doApply() if (tx.isFlag(tfLoanUnimpair)) return unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_); // Noop, as described above. + + associateAsset(*loanSle, vaultAsset); + associateAsset(*brokerSle, vaultAsset); + associateAsset(*vaultSle, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index f3973d9488..13f62d4c0d 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -485,6 +486,16 @@ LoanPay::doApply() coverAvailableProxy += totalPaidToBroker; } + associateAsset(*loanSle, asset); + associateAsset(*brokerSle, asset); + associateAsset(*vaultSle, asset); + + // Duplicate some checks after rounding + XRPL_ASSERT_PARTS( + *assetsAvailableProxy <= *assetsTotalProxy, + "xrpl::LoanPay::doApply", + "assets available must not be greater than assets outstanding"); + #if !NDEBUG auto const accountBalanceBefore = accountHolds( view, diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 4c14cde421..0b83d3009f 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -2,6 +2,7 @@ // #include +#include #include namespace xrpl { @@ -301,17 +302,15 @@ LoanSet::preclaim(PreclaimContext const& ctx) // This check is almost duplicated in doApply, but that check is done after // the overall loan scale is known. This is mostly only relevant for // integral (non-IOU) types + for (auto const& field : getValueFields()) { - for (auto const& field : getValueFields()) + if (auto const value = tx[field]; + value && STAmount{asset, *value} != *value) { - if (auto const value = tx[field]; - value && STAmount{asset, *value} != *value) - { - JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value - << ") can not be represented as a(n) " - << to_string(asset) << "."; - return tecPRECISION_LOSS; - } + JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value + << ") can not be represented as a(n) " + << to_string(asset) << "."; + return tecPRECISION_LOSS; } } @@ -434,19 +433,16 @@ LoanSet::doApply() } // Check that relevant values won't lose precision. This is mostly only // relevant for IOU assets. + for (auto const& field : getValueFields()) { - for (auto const& field : getValueFields()) + if (auto const value = tx[field]; + value && !isRounded(vaultAsset, *value, properties.loanScale)) { - if (auto const value = tx[field]; - value && !isRounded(vaultAsset, *value, properties.loanScale)) - { - JLOG(j_.warn()) - << field.f->getName() << " (" << *value - << ") has too much precision. Total loan value is " - << properties.loanState.valueOutstanding - << " with a scale of " << properties.loanScale; - return tecPRECISION_LOSS; - } + JLOG(j_.warn()) << field.f->getName() << " (" << *value + << ") has too much precision. Total loan value is " + << properties.loanState.valueOutstanding + << " with a scale of " << properties.loanScale; + return tecPRECISION_LOSS; } } @@ -649,6 +645,10 @@ LoanSet::doApply() if (auto const ter = dirLink(view, borrower, loan, sfOwnerNode)) return ter; + associateAsset(*vaultSle, vaultAsset); + associateAsset(*brokerSle, vaultAsset); + associateAsset(*loan, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index a834f7c6c3..691443ed93 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -1136,6 +1136,10 @@ Transactor::operator()() { JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID(); + // These global updates really should have been for every Transaction + // step: preflight, preclaim, and doApply. And even calculateBaseFee. See + // with_txn_type(). + // // raii classes for the current ledger rules. // fixUniversalNumber predate the rulesGuard and should be replaced. NumberSO stNumberSO{view().rules().enabled(fixUniversalNumber)}; @@ -1152,7 +1156,7 @@ Transactor::operator()() { // LCOV_EXCL_START JLOG(j_.fatal()) << "Transaction serdes mismatch"; - JLOG(j_.info()) << to_string(ctx_.tx.getJson(JsonOptions::none)); + JLOG(j_.fatal()) << ctx_.tx.getJson(JsonOptions::none); JLOG(j_.fatal()) << s2.getJson(JsonOptions::none); UNREACHABLE( "xrpl::Transactor::operator() : transaction serdes mismatch"); diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index 2552e8c1ff..dbdf4440ec 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -457,6 +458,8 @@ VaultClawback::doApply() } } + associateAsset(*vault, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index 893a1108fa..402d877a00 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -230,6 +231,8 @@ VaultCreate::doApply() return err; } + associateAsset(*vault, asset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/VaultDelete.cpp b/src/xrpld/app/tx/detail/VaultDelete.cpp index 756e7b94e6..9b63c7766b 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.cpp +++ b/src/xrpld/app/tx/detail/VaultDelete.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -85,6 +86,7 @@ VaultDelete::doApply() // Destroy the asset holding. auto asset = vault->at(sfAsset); + if (auto ter = removeEmptyHolding(view(), vault->at(sfAccount), asset, j_); !isTesSuccess(ter)) return ter; @@ -205,6 +207,8 @@ VaultDelete::doApply() // Destroy the vault. view().erase(vault); + associateAsset(*vault, asset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 51b38afc36..02ef4afad1 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -134,6 +135,7 @@ VaultDeposit::doApply() auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE + auto const vaultAsset = vault->at(sfAsset); auto const amount = ctx_.tx[sfAmount]; // Make sure the depositor can hold shares. @@ -282,6 +284,8 @@ VaultDeposit::doApply() !isTesSuccess(ter)) return ter; + associateAsset(*vault, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index 648ac12c3d..13c8ad5db8 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -128,6 +129,8 @@ VaultSet::doApply() if (!vault) return tefINTERNAL; // LCOV_EXCL_LINE + auto const vaultAsset = vault->at(sfAsset); + auto const mptIssuanceID = (*vault)[sfShareMPTID]; auto const sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID)); if (!sleIssuance) @@ -172,6 +175,8 @@ VaultSet::doApply() // to verify the operation. view().update(vault); + associateAsset(*vault, vaultAsset); + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index f8b7a1a739..9a4334e435 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -115,6 +116,7 @@ VaultWithdraw::doApply() auto const amount = ctx_.tx[sfAmount]; Asset const vaultAsset = vault->at(sfAsset); + MPTIssue const share{mptIssuanceID}; STAmount sharesRedeemed = {share}; STAmount assetsWithdrawn; @@ -239,6 +241,8 @@ VaultWithdraw::doApply() auto const dstAcct = ctx_.tx[~sfDestination].value_or(account_); + associateAsset(*vault, vaultAsset); + return doWithdraw( view(), ctx_.tx, diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index e0bd9d0d2d..0fae1a15e0 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -34,8 +34,38 @@ struct UnknownTxnType : std::exception // throw an "UnknownTxnType" exception on error template auto -with_txn_type(TxType txnType, F&& f) +with_txn_type(Rules const& rules, TxType txnType, F&& f) { + // These global updates really should have been for every Transaction + // step: preflight, preclaim, calculateBaseFee, and doApply. Unfortunately, + // they were only included in doApply (via Transactor::operator()). That may + // have been sufficient when the changes were only related to operations + // that mutated data, but some features will now change how they read data, + // so these need to be more global. + // + // To prevent unintentional side effects on existing checks, they will be + // set for every operation only once SingleAssetVault (or later + // LendingProtocol) are enabled. + // + // See also Transactor::operator(). + // + std::optional stNumberSO; + std::optional rulesGuard; + std::optional mantissaScaleGuard; + if (rules.enabled(featureSingleAssetVault) || + rules.enabled(featureLendingProtocol)) + { + // raii classes for the current ledger rules. + // fixUniversalNumber predates the rulesGuard and should be replaced. + stNumberSO.emplace(rules.enabled(fixUniversalNumber)); + rulesGuard.emplace(rules); + } + else + { + // Without those features enabled, always use the old number rules. + mantissaScaleGuard.emplace(MantissaRange::small); + } + switch (txnType) { #pragma push_macro("TRANSACTION") @@ -99,7 +129,7 @@ invoke_preflight(PreflightContext const& ctx) { try { - return with_txn_type(ctx.tx.getTxnType(), [&]() { + return with_txn_type(ctx.rules, ctx.tx.getTxnType(), [&]() { auto const tec = Transactor::invokePreflight(ctx); return std::make_pair( tec, @@ -126,50 +156,51 @@ invoke_preclaim(PreclaimContext const& ctx) { // use name hiding to accomplish compile-time polymorphism of static // class functions for Transactor and derived classes. - return with_txn_type(ctx.tx.getTxnType(), [&]() -> TER { - // preclaim functionality is divided into two sections: - // 1. Up to and including the signature check: returns NotTEC. - // All transaction checks before and including checkSign - // MUST return NotTEC, or something more restrictive. - // Allowing tec results in these steps risks theft or - // destruction of funds, as a fee will be charged before the - // signature is checked. - // 2. After the signature check: returns TER. + return with_txn_type( + ctx.view.rules(), ctx.tx.getTxnType(), [&]() -> TER { + // preclaim functionality is divided into two sections: + // 1. Up to and including the signature check: returns NotTEC. + // All transaction checks before and including checkSign + // MUST return NotTEC, or something more restrictive. + // Allowing tec results in these steps risks theft or + // destruction of funds, as a fee will be charged before the + // signature is checked. + // 2. After the signature check: returns TER. - // If the transactor requires a valid account and the - // transaction doesn't list one, preflight will have already - // a flagged a failure. - auto const id = ctx.tx.getAccountID(sfAccount); + // If the transactor requires a valid account and the + // transaction doesn't list one, preflight will have already + // a flagged a failure. + auto const id = ctx.tx.getAccountID(sfAccount); - if (id != beast::zero) - { - if (NotTEC const preSigResult = [&]() -> NotTEC { - if (NotTEC const result = - T::checkSeqProxy(ctx.view, ctx.tx, ctx.j)) - return result; + if (id != beast::zero) + { + if (NotTEC const preSigResult = [&]() -> NotTEC { + if (NotTEC const result = + T::checkSeqProxy(ctx.view, ctx.tx, ctx.j)) + return result; - if (NotTEC const result = - T::checkPriorTxAndLastLedger(ctx)) - return result; + if (NotTEC const result = + T::checkPriorTxAndLastLedger(ctx)) + return result; - if (NotTEC const result = - T::checkPermission(ctx.view, ctx.tx)) - return result; + if (NotTEC const result = + T::checkPermission(ctx.view, ctx.tx)) + return result; - if (NotTEC const result = T::checkSign(ctx)) - return result; + if (NotTEC const result = T::checkSign(ctx)) + return result; - return tesSUCCESS; - }()) - return preSigResult; + return tesSUCCESS; + }()) + return preSigResult; - if (TER const result = - T::checkFee(ctx, calculateBaseFee(ctx.view, ctx.tx))) - return result; - } + if (TER const result = T::checkFee( + ctx, calculateBaseFee(ctx.view, ctx.tx))) + return result; + } - return T::preclaim(ctx); - }); + return T::preclaim(ctx); + }); } catch (UnknownTxnType const& e) { @@ -204,7 +235,7 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) { try { - return with_txn_type(tx.getTxnType(), [&]() { + return with_txn_type(view.rules(), tx.getTxnType(), [&]() { return T::calculateBaseFee(view, tx); }); } @@ -263,10 +294,11 @@ invoke_apply(ApplyContext& ctx) { try { - return with_txn_type(ctx.tx.getTxnType(), [&]() { - T p(ctx); - return p(); - }); + return with_txn_type( + ctx.view().rules(), ctx.tx.getTxnType(), [&]() { + T p(ctx); + return p(); + }); } catch (UnknownTxnType const& e) {