Compare commits

...

27 Commits

Author SHA1 Message Date
Ed Hennis
7c499aacab Add unit tests for normalizeToRange
- Steal changes from @pratik's #6150 to avoid UB
2026-01-28 20:34:58 -05:00
Ed Hennis
0364d61b42 Clean-ups and tweaks 2026-01-28 20:34:58 -05:00
Ed Hennis
f0eef6ec63 Reduce expensive(?) accesses to thread_local MantissaRange 2026-01-28 20:34:58 -05:00
Ed Hennis
7e2c2573d2 Fix bugs
- Simplify shiftExponent().
- Clean up to_string() to prevent integers from including "e0".
- Fix root() and root2() computations by ensuring the mantissas have
  a consistent length.
2026-01-28 20:34:55 -05:00
Ed Hennis
a48182c65d Convert "bool negative_ & uint64_t mantissa_" combo back to "rep mantissa_" 2026-01-28 20:33:42 -05:00
Ed Hennis
e7f81c2646 Remove the _ suffixes from doNormalize function parameters 2026-01-28 20:33:42 -05:00
Ed Hennis
5f8acbc001 Use 2^63-1 as maxMantissa for large range
- That makes minMantissa 2^63/10+1.
- Simplifies many of the existing operations, and removes the need for
  the accessors (mantissa() & exponent()) to do any math.
2026-01-28 20:33:42 -05:00
Ed Hennis
47bfd15004 Merge branch 'tapanito/lending-vault-invariant' into ximinez/number-scale 2026-01-28 18:40:32 -04:00
Ed Hennis
accbe7343d Change ValidVault::DeltaInfo::scale to an optional 2026-01-22 19:54:41 -05:00
Ed Hennis
cd4f915e40 Update src/xrpld/app/tx/detail/InvariantCheck.cpp
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
2026-01-22 18:27:29 -05:00
Vito Tumas
f8d441bb6b Merge branch 'develop' into tapanito/lending-vault-invariant 2026-01-22 23:36:25 +01:00
Ed Hennis
2f0690f3c5 Merge remote-tracking branch 'XRPLF/tapanito/lending-vault-invariant' into ximinez/number-scale
* XRPLF/tapanito/lending-vault-invariant:
  refactors vault invariant to use relative distance
  Limit reply size on `TMGetObjectByHash` queries (6110)
  ci: remove 'master' branch as a trigger (6234)
  Improve ledger_entry lookups for fee, amendments, NUNL, and hashes (5644)
2026-01-21 15:14:38 -05:00
Ed Hennis
040bf34257 ValidVault tracks scale of original operands alongside deltas 2026-01-21 14:21:58 -05:00
Vito Tumas
5c87c4ffb0 Merge branch 'develop' into tapanito/lending-vault-invariant 2026-01-21 18:58:09 +01:00
Vito
0a9436def4 refactors vault invariant to use relative distance 2026-01-21 18:57:33 +01:00
Ed Hennis
e5646e4ebe Merge remote-tracking branch 'XRPLF/tapanito/lending-vault-invariant' into ximinez/number-scale
* XRPLF/tapanito/lending-vault-invariant:
  flyby change removing unused includes
  addreses review comments
  adds invariant test
2026-01-21 12:48:48 -05:00
Vito
f76bf5340c flyby change removing unused includes 2026-01-21 11:50:38 +01:00
Vito
1af0f4bd43 addreses review comments 2026-01-21 11:50:18 +01:00
Vito
c6821ab842 adds invariant test 2026-01-21 11:42:03 +01:00
Ed Hennis
7ab9709373 Add canonical "scale" computation to Number
- Requires a template for STAmount and Asset.
- Update tests and computeMinScale from #6217 to use scale.
- Convert a few other places to use "scale" correctly.
2026-01-20 20:06:45 -05:00
Vito
aa12210fcd fixes a minor min bug 2026-01-20 18:01:14 +01:00
Vito
9235ec483a adds missing incldues 2026-01-20 17:06:23 +01:00
Vito Tumas
ffe0a3cc61 Merge branch 'develop' into tapanito/lending-vault-invariant 2026-01-16 11:26:28 +01:00
Vito
add9071b20 fixes formatting 2026-01-16 11:26:12 +01:00
Vito Tumas
465e7b6d91 Merge branch 'develop' into tapanito/lending-vault-invariant 2026-01-15 16:10:25 +01:00
Vito
6223ebe05e improves VaultWithdraw invariant rounding 2026-01-15 16:09:13 +01:00
Vito
4fe50c2d31 attempt to fix rounding issues 2026-01-14 20:58:04 +01:00
13 changed files with 1651 additions and 487 deletions

View File

@@ -10,6 +10,10 @@
#include <ostream> #include <ostream>
#include <string> #include <string>
#ifdef _MSC_VER
#include <boost/multiprecision/cpp_int.hpp>
#endif // !defined(_MSC_VER)
namespace xrpl { namespace xrpl {
class Number; class Number;
@@ -17,18 +21,37 @@ class Number;
std::string std::string
to_string(Number const& amount); to_string(Number const& amount);
/** Returns a rough estimate of log10(value).
*
* The return value is a pair (log, rem), where log is the estimated log10,
* and rem is value divided by 10^log. If rem is 1, then value is an exact
* power of ten, and log is the exact log10(value).
*
* This function only works for positive values.
*/
template <typename T>
constexpr std::pair<int, T>
logTenEstimate(T value)
{
int log = 0;
T remainder = value;
while (value >= 10)
{
if (value % 10 == 0)
remainder = remainder / 10;
value /= 10;
++log;
}
return {log, remainder};
}
template <typename T> template <typename T>
constexpr std::optional<int> constexpr std::optional<int>
logTen(T value) logTen(T value)
{ {
int log = 0; auto const est = logTenEstimate(value);
while (value >= 10 && value % 10 == 0) if (est.second == 1)
{ return est.first;
value /= 10;
++log;
}
if (value == 1)
return log;
return std::nullopt; return std::nullopt;
} }
@@ -42,12 +65,10 @@ isPowerOfTen(T value)
/** MantissaRange defines a range for the mantissa of a normalized Number. /** MantissaRange defines a range for the mantissa of a normalized Number.
* *
* The mantissa is in the range [min, max], where * 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". * The mantissa_scale enum indicates whether the range is "small" or "large".
* This intentionally restricts the number of MantissaRanges that can be * This intentionally restricts the number of MantissaRanges that can be
* instantiated to two: one for each scale. * used to two: one for each scale.
* *
* The "small" scale is based on the behavior of STAmount for IOUs. It has a min * 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 * value of 10^15, and a max value of 10^16-1. This was sufficient for
@@ -61,8 +82,8 @@ isPowerOfTen(T value)
* "large" scale. * "large" scale.
* *
* The "large" scale is intended to represent all values that can be represented * 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 * by an STAmount - IOUs, XRP, and MPTs. It has a min value of 2^63/10+1
* value of 10^19-1. * (truncated), and a max value of 2^63-1.
* *
* Note that if the mentioned amendments are eventually retired, this class * 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 * should be left in place, but the "small" scale option should be removed. This
@@ -74,28 +95,50 @@ struct MantissaRange
enum mantissa_scale { small, large }; enum mantissa_scale { small, large };
explicit constexpr MantissaRange(mantissa_scale scale_) explicit constexpr MantissaRange(mantissa_scale scale_)
: min(getMin(scale_)) : max(getMax(scale_))
, max(min * 10 - 1) , min(computeMin(max))
, log(logTen(min).value_or(-1)) , referenceMin(getReferenceMin(scale_, min))
, log(computeLog(min))
, scale(scale_) , scale(scale_)
{ {
// Since this is constexpr, if any of these throw, it won't compile
if (min * 10 <= max)
throw std::out_of_range("min * 10 <= max");
if (max / 10 >= min)
throw std::out_of_range("max / 10 >= min");
if ((min - 1) * 10 > max)
throw std::out_of_range("(min - 1) * 10 > max");
// This is a little hacky
if ((max + 10) / 10 < min)
throw std::out_of_range("(max + 10) / 10 < min");
} }
rep min; // Explicitly delete copy and move operations
MantissaRange(MantissaRange const&) = delete;
MantissaRange(MantissaRange&&) = delete;
MantissaRange&
operator=(MantissaRange const&) = delete;
MantissaRange&
operator=(MantissaRange&&) = delete;
rep max; rep max;
rep min;
// This is not a great name. Used to determine if mantissas are in range,
// but have fewer digits than max
rep referenceMin;
int log; int log;
mantissa_scale scale; mantissa_scale scale;
private: private:
static constexpr rep static constexpr rep
getMin(mantissa_scale scale_) getMax(mantissa_scale scale)
{ {
switch (scale_) switch (scale)
{ {
case small: case small:
return 1'000'000'000'000'000ULL; return 9'999'999'999'999'999ULL;
case large: case large:
return 1'000'000'000'000'000'000ULL; return std::numeric_limits<std::int64_t>::max();
default: default:
// Since this can never be called outside a non-constexpr // Since this can never be called outside a non-constexpr
// context, this throw assures that the build fails if an // context, this throw assures that the build fails if an
@@ -103,6 +146,33 @@ private:
throw std::runtime_error("Unknown mantissa scale"); throw std::runtime_error("Unknown mantissa scale");
} }
} }
static constexpr rep
computeMin(rep max)
{
return max / 10 + 1;
}
static constexpr rep
getReferenceMin(mantissa_scale scale, rep min)
{
switch (scale)
{
case large:
return 1'000'000'000'000'000'000ULL;
default:
if (isPowerOfTen(min))
return min;
throw std::runtime_error("Unknown/bad mantissa scale");
}
}
static constexpr rep
computeLog(rep min)
{
auto const estimate = logTenEstimate(min);
return estimate.first + (estimate.second == 1 ? 0 : 1);
}
}; };
// Like std::integral, but only 64-bit integral types. // Like std::integral, but only 64-bit integral types.
@@ -110,6 +180,24 @@ template <class T>
concept Integral64 = concept Integral64 =
std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::uint64_t>; std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::uint64_t>;
template <class STAmount, class Asset>
concept CanUseAsScale = requires(Asset a, Number n) { STAmount(a, n); } &&
requires(STAmount s) { s.exponent(); };
namespace detail {
#ifdef _MSC_VER
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)
template <class T>
concept UnsignedMantissa =
std::is_unsigned_v<T> || std::is_same_v<T, uint128_t>;
} // namespace detail
/** Number is a floating point type that can represent a wide range of values. /** 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 - * It can represent all values that can be represented by an STAmount -
@@ -137,9 +225,7 @@ concept Integral64 =
* 1. Normalization can be disabled by using the "unchecked" ctor tag. This * 1. Normalization can be disabled by using the "unchecked" ctor tag. This
* should only be used at specific conversion points, some constexpr * should only be used at specific conversion points, some constexpr
* values, and in unit tests. * values, and in unit tests.
* 2. The max of the "large" range, 10^19-1, is the largest 10^X-1 value that * 2. The max of the "large" range, 2^63-1, TODO: explain the large range.
* 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 ---- * ---- External Interface ----
* *
@@ -153,7 +239,7 @@ concept Integral64 =
* *
* Note: * Note:
* 1. 2^63-1 is between 10^18 and 10^19-1, which are the limits of the "large" * 1. 2^63-1 is between 10^18 and 10^19-1, which are the limits of the "large"
* mantissa range. * mantissa range. TODO: update this explanation.
* 2. The functions mantissa() and exponent() return the external view of the * 2. The functions mantissa() and exponent() return the external view of the
* Number value, specifically using a signed 63-bit mantissa. This may * Number value, specifically using a signed 63-bit mantissa. This may
* require altering the internal representation to fit into that range * require altering the internal representation to fit into that range
@@ -213,8 +299,7 @@ class Number
using rep = std::int64_t; using rep = std::int64_t;
using internalrep = MantissaRange::rep; using internalrep = MantissaRange::rep;
bool negative_{false}; rep mantissa_{0};
internalrep mantissa_{0};
int exponent_{std::numeric_limits<int>::lowest()}; int exponent_{std::numeric_limits<int>::lowest()};
public: public:
@@ -222,9 +307,11 @@ public:
constexpr static int minExponent = -32768; constexpr static int minExponent = -32768;
constexpr static int maxExponent = 32768; constexpr static int maxExponent = 32768;
#if MAXREP
constexpr static internalrep maxRep = std::numeric_limits<rep>::max(); constexpr static internalrep maxRep = std::numeric_limits<rep>::max();
static_assert(maxRep == 9'223'372'036'854'775'807); static_assert(maxRep == 9'223'372'036'854'775'807);
static_assert(-maxRep == std::numeric_limits<rep>::min() + 1); static_assert(-maxRep == std::numeric_limits<rep>::min() + 1);
#endif
// May need to make unchecked private // May need to make unchecked private
struct unchecked struct unchecked
@@ -269,6 +356,26 @@ public:
constexpr int constexpr int
exponent() const noexcept; exponent() const noexcept;
/** Get the scale of this Number for the given asset.
*
* "scale" is similar to "exponent", but from the perspective of STAmount,
* which has different rules for determining the exponent than Number.
*
* Because Number does not have access to STAmount or Asset, this function
* is implemented as a template, with the expectation that it will only be
* used by those types. Any types that fit the requirements will work,
* though, if there's a need.
*
* @tparam STAmount The STAmount type.
* @tparam Asset The Asset type.
* @param asset The asset to use for determining the scale.
* @return The scale of this Number for the given asset.
*/
template <class STAmount, class Asset>
int
scale(Asset const& asset) const
requires CanUseAsScale<STAmount, Asset>;
constexpr Number constexpr Number
operator+() const noexcept; operator+() const noexcept;
constexpr Number constexpr Number
@@ -310,8 +417,7 @@ public:
friend constexpr bool friend constexpr bool
operator==(Number const& x, Number const& y) noexcept operator==(Number const& x, Number const& y) noexcept
{ {
return x.negative_ == y.negative_ && x.mantissa_ == y.mantissa_ && return x.mantissa_ == y.mantissa_ && x.exponent_ == y.exponent_;
x.exponent_ == y.exponent_;
} }
friend constexpr bool friend constexpr bool
@@ -325,8 +431,8 @@ public:
{ {
// If the two amounts have different signs (zero is treated as positive) // If the two amounts have different signs (zero is treated as positive)
// then the comparison is true iff the left is negative. // then the comparison is true iff the left is negative.
bool const lneg = x.negative_; bool const lneg = x.mantissa_ < 0;
bool const rneg = y.negative_; bool const rneg = y.mantissa_ < 0;
if (lneg != rneg) if (lneg != rneg)
return lneg; return lneg;
@@ -354,7 +460,7 @@ public:
constexpr int constexpr int
signum() const noexcept signum() const noexcept
{ {
return negative_ ? -1 : (mantissa_ ? 1 : 0); return mantissa_ < 0 ? -1 : (mantissa_ ? 1 : 0);
} }
Number Number
@@ -393,6 +499,9 @@ public:
friend Number friend Number
root2(Number f); root2(Number f);
friend Number
power(Number const& f, unsigned n, unsigned d);
// Thread local rounding control. Default is to_nearest // Thread local rounding control. Default is to_nearest
enum rounding_mode { to_nearest, towards_zero, downward, upward }; enum rounding_mode { to_nearest, towards_zero, downward, upward };
static rounding_mode static rounding_mode
@@ -457,22 +566,48 @@ private:
static_assert(isPowerOfTen(smallRange.min)); static_assert(isPowerOfTen(smallRange.min));
static_assert(smallRange.min == 1'000'000'000'000'000LL); static_assert(smallRange.min == 1'000'000'000'000'000LL);
static_assert(smallRange.max == 9'999'999'999'999'999LL); static_assert(smallRange.max == 9'999'999'999'999'999LL);
static_assert(smallRange.referenceMin == smallRange.min);
static_assert(smallRange.log == 15); static_assert(smallRange.log == 15);
#if MAXREP
static_assert(smallRange.min < maxRep); static_assert(smallRange.min < maxRep);
static_assert(smallRange.max < maxRep); static_assert(smallRange.max < maxRep);
#endif
constexpr static MantissaRange largeRange{MantissaRange::large}; constexpr static MantissaRange largeRange{MantissaRange::large};
static_assert(isPowerOfTen(largeRange.min)); static_assert(!isPowerOfTen(largeRange.min));
static_assert(largeRange.min == 1'000'000'000'000'000'000ULL); static_assert(largeRange.min == 922'337'203'685'477'581ULL);
static_assert(largeRange.max == internalrep(9'999'999'999'999'999'999ULL)); static_assert(largeRange.max == internalrep(9'223'372'036'854'775'807ULL));
static_assert(largeRange.max == std::numeric_limits<rep>::max());
static_assert(largeRange.referenceMin == 1'000'000'000'000'000'000ULL);
static_assert(largeRange.log == 18); static_assert(largeRange.log == 18);
// There are 2 values that will not fit in largeRange without some extra
// work
// * 9223372036854775808
// * 9223372036854775809
// They both end up < min, but with a leftover. If they round up, everything
// will be fine. If they don't, well need to bring them up into range.
// Guard::bringIntoRange handles this situation.
#if MAXREP
static_assert(largeRange.min < maxRep); static_assert(largeRange.min < maxRep);
static_assert(largeRange.max > maxRep); static_assert(largeRange.max > maxRep);
#endif
// The range for the mantissa when normalized. // The range for the mantissa when normalized.
// Use reference_wrapper to avoid making copies, and prevent accidentally // Use reference_wrapper to avoid making copies, and prevent accidentally
// changing the values inside the range. // changing the values inside the range.
static thread_local std::reference_wrapper<MantissaRange const> range_; static thread_local std::reference_wrapper<MantissaRange const> range_;
// And one is needed because it needs to choose between oneSmall and
// oneLarge based on the current range
static Number
one(MantissaRange const& range);
static Number
root(MantissaRange const& range, Number f, unsigned d);
void
normalize(MantissaRange const& range);
void void
normalize(); normalize();
@@ -495,11 +630,14 @@ private:
friend void friend void
doNormalize( doNormalize(
bool& negative, bool& negative,
T& mantissa_, T& mantissa,
int& exponent_, int& exponent,
MantissaRange::rep const& minMantissa, MantissaRange::rep const& minMantissa,
MantissaRange::rep const& maxMantissa); MantissaRange::rep const& maxMantissa);
bool
isnormal(MantissaRange const& range) const noexcept;
bool bool
isnormal() const noexcept; isnormal() const noexcept;
@@ -516,7 +654,64 @@ private:
static internalrep static internalrep
externalToInternal(rep mantissa); externalToInternal(rep mantissa);
/** Breaks down the number into components, potentially de-normalizing it.
*
* Ensures that the mantissa always has range_.log + 1 digits.
*
*/
template <detail::UnsignedMantissa Rep = internalrep>
std::tuple<bool, Rep, int>
toInternal(MantissaRange const& range) const;
/** Breaks down the number into components, potentially de-normalizing it.
*
* Ensures that the mantissa always has range_.log + 1 digits.
*
*/
template <detail::UnsignedMantissa Rep = internalrep>
std::tuple<bool, Rep, int>
toInternal() const;
/** Rebuilds the number from components.
*
* If "normalized" is true, the values are expected to be normalized - all
* in their valid ranges.
*
* If "normalized" is false, the values are expected to be "near
* normalized", meaning that the mantissa has to be modified at most once to
* bring it back into range.
*
*/
template <
bool expectNormal = true,
detail::UnsignedMantissa Rep = internalrep>
void
fromInternal(
bool negative,
Rep mantissa,
int exponent,
MantissaRange const* pRange);
/** Rebuilds the number from components.
*
* If "normalized" is true, the values are expected to be normalized - all
* in their valid ranges.
*
* If "normalized" is false, the values are expected to be "near
* normalized", meaning that the mantissa has to be modified at most once to
* bring it back into range.
*
*/
template <
bool expectNormal = true,
detail::UnsignedMantissa Rep = internalrep>
void
fromInternal(bool negative, Rep mantissa, int exponent);
class Guard; class Guard;
public:
constexpr static internalrep largestMantissa = largeRange.max;
}; };
inline constexpr Number::Number( inline constexpr Number::Number(
@@ -524,7 +719,8 @@ inline constexpr Number::Number(
internalrep mantissa, internalrep mantissa,
int exponent, int exponent,
unchecked) noexcept unchecked) noexcept
: negative_(negative), mantissa_{mantissa}, exponent_{exponent} : mantissa_{(negative ? -1 : 1) * static_cast<rep>(mantissa)}
, exponent_{exponent}
{ {
} }
@@ -538,16 +734,6 @@ inline constexpr Number::Number(
constexpr static Number numZero{}; constexpr static Number numZero{};
inline Number::Number(
bool negative,
internalrep mantissa,
int exponent,
normalized)
: Number(negative, mantissa, exponent, unchecked{})
{
normalize();
}
inline Number::Number(internalrep mantissa, int exponent, normalized) inline Number::Number(internalrep mantissa, int exponent, normalized)
: Number(false, mantissa, exponent, normalized{}) : Number(false, mantissa, exponent, normalized{})
{ {
@@ -570,17 +756,7 @@ inline Number::Number(rep mantissa) : Number{mantissa, 0}
inline constexpr Number::rep inline constexpr Number::rep
Number::mantissa() const noexcept Number::mantissa() const noexcept
{ {
auto m = mantissa_; return 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<Number::rep>(m);
} }
/** Returns the exponent of the external view of the Number. /** Returns the exponent of the external view of the Number.
@@ -591,16 +767,15 @@ Number::mantissa() const noexcept
inline constexpr int inline constexpr int
Number::exponent() const noexcept Number::exponent() const noexcept
{ {
auto e = exponent_; return exponent_;
if (mantissa_ > maxRep) }
{
XRPL_ASSERT_PARTS( template <class STAmount, class Asset>
!isnormal() || (mantissa_ % 10 == 0 && mantissa_ / 10 <= maxRep), int
"xrpl::Number::exponent", Number::scale(Asset const& asset) const
"large normalized mantissa has no remainder"); requires CanUseAsScale<STAmount, Asset>
++e; {
} return STAmount{asset, *this}.exponent();
return e;
} }
inline constexpr Number inline constexpr Number
@@ -615,7 +790,7 @@ Number::operator-() const noexcept
if (mantissa_ == 0) if (mantissa_ == 0)
return Number{}; return Number{};
auto x = *this; auto x = *this;
x.negative_ = !x.negative_; x.mantissa_ = -1 * x.mantissa_;
return x; return x;
} }
@@ -696,45 +871,62 @@ Number::min() noexcept
inline Number inline Number
Number::max() noexcept Number::max() noexcept
{ {
return Number{ return Number{false, range_.get().max, maxExponent, unchecked{}};
false, std::min(range_.get().max, maxRep), maxExponent, unchecked{}};
} }
inline Number inline Number
Number::lowest() noexcept Number::lowest() noexcept
{ {
return Number{ return Number{true, range_.get().max, maxExponent, unchecked{}};
true, std::min(range_.get().max, maxRep), maxExponent, unchecked{}}; }
inline bool
Number::isnormal(MantissaRange const& range) const noexcept
{
auto const abs_m = mantissa_ < 0 ? -mantissa_ : mantissa_;
return *this == Number{} ||
(range.min <= abs_m && abs_m <= range.max && //
minExponent <= exponent_ && exponent_ <= maxExponent);
} }
inline bool inline bool
Number::isnormal() const noexcept Number::isnormal() const noexcept
{ {
MantissaRange const& range = range_; return isnormal(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 <Integral64 T> template <Integral64 T>
std::pair<T, int> std::pair<T, int>
Number::normalizeToRange(T minMantissa, T maxMantissa) const Number::normalizeToRange(T minMantissa, T maxMantissa) const
{ {
bool negative = negative_; bool negative = mantissa_ < 0;
internalrep mantissa = mantissa_; auto const sign = negative ? -1 : 1;
internalrep mantissa = sign * mantissa_;
int exponent = exponent_; int exponent = exponent_;
if constexpr (std::is_unsigned_v<T>) if constexpr (std::is_unsigned_v<T>)
{
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
!negative, !negative,
"xrpl::Number::normalizeToRange", "xrpl::Number::normalizeToRange",
"Number is non-negative for unsigned range."); "Number is non-negative for unsigned range.");
// To avoid logical errors in release builds, throw if the Number is
// negative for an unsigned range.
if (negative)
throw std::runtime_error(
"Number::normalizeToRange: Number is negative for "
"unsigned range.");
}
Number::normalize(negative, mantissa, exponent, minMantissa, maxMantissa); Number::normalize(negative, mantissa, exponent, minMantissa, maxMantissa);
auto const sign = negative ? -1 : 1; // Cast mantissa to signed type first (if T is a signed type) to avoid
return std::make_pair(static_cast<T>(sign * mantissa), exponent); // unsigned integer overflow when multiplying by negative sign
T signedMantissa = static_cast<T>(mantissa);
if (negative)
signedMantissa = -signedMantissa;
return std::make_pair(signedMantissa, exponent);
return std::make_pair(sign * static_cast<T>(mantissa), exponent);
} }
inline constexpr Number inline constexpr Number

View File

@@ -233,7 +233,7 @@ std::size_t constexpr maxMPTokenMetadataLength = 1024;
/** The maximum amount of MPTokenIssuance */ /** The maximum amount of MPTokenIssuance */
std::uint64_t constexpr maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull; std::uint64_t constexpr maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull;
static_assert(Number::maxRep >= maxMPTokenAmount); static_assert(Number::largestMantissa >= maxMPTokenAmount);
/** The maximum length of Data payload */ /** The maximum length of Data payload */
std::size_t constexpr maxDataPayloadLength = 256; std::size_t constexpr maxDataPayloadLength = 256;

View File

@@ -568,6 +568,10 @@ STAmount::fromNumber(A const& a, Number const& number)
return STAmount{asset, intValue, 0, negative}; return STAmount{asset, intValue, 0, negative};
} }
XRPL_ASSERT_PARTS(
working.signum() >= 0,
"ripple::STAmount::fromNumber",
"non-negative Number to normalize");
auto const [mantissa, exponent] = auto const [mantissa, exponent] =
working.normalizeToRange(cMinValue, cMaxValue); working.normalizeToRange(cMinValue, cMaxValue);

View File

@@ -24,7 +24,7 @@ systemName()
/** Number of drops in the genesis account. */ /** Number of drops in the genesis account. */
constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP}; constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP};
static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000); static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000);
static_assert(Number::maxRep >= INITIAL_XRP.drops()); static_assert(Number::largestMantissa >= INITIAL_XRP.drops());
/** Returns true if the amount does not exceed the initial XRP in existence. */ /** Returns true if the amount does not exceed the initial XRP in existence. */
inline bool inline bool

View File

@@ -11,18 +11,16 @@
#include <numeric> #include <numeric>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <string_view>
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
#ifdef _MSC_VER #ifdef _MSC_VER
#pragma message("Using boost::multiprecision::uint128_t and int128_t") #pragma message("Using boost::multiprecision::uint128_t and int128_t")
#include <boost/multiprecision/cpp_int.hpp> #endif
using uint128_t = boost::multiprecision::uint128_t;
using int128_t = boost::multiprecision::int128_t; using uint128_t = ripple::detail::uint128_t;
#else // !defined(_MSC_VER) using int128_t = ripple::detail::int128_t;
using uint128_t = __uint128_t;
using int128_t = __int128_t;
#endif // !defined(_MSC_VER)
namespace xrpl { namespace xrpl {
@@ -62,10 +60,6 @@ Number::setMantissaScale(MantissaRange::mantissa_scale scale)
// precision to an operation. This enables the final result // precision to an operation. This enables the final result
// to be correctly rounded to the internal precision of Number. // to be correctly rounded to the internal precision of Number.
template <class T>
concept UnsignedMantissa =
std::is_unsigned_v<T> || std::is_same_v<T, uint128_t>;
class Number::Guard class Number::Guard
{ {
std::uint64_t digits_; // 16 decimal guard digits std::uint64_t digits_; // 16 decimal guard digits
@@ -101,7 +95,7 @@ public:
round() noexcept; round() noexcept;
// Modify the result to the correctly rounded value // Modify the result to the correctly rounded value
template <UnsignedMantissa T> template <detail::UnsignedMantissa T>
void void
doRoundUp( doRoundUp(
bool& negative, bool& negative,
@@ -109,10 +103,10 @@ public:
int& exponent, int& exponent,
internalrep const& minMantissa, internalrep const& minMantissa,
internalrep const& maxMantissa, internalrep const& maxMantissa,
std::string location); std::string_view location);
// Modify the result to the correctly rounded value // Modify the result to the correctly rounded value
template <UnsignedMantissa T> template <detail::UnsignedMantissa T>
void void
doRoundDown( doRoundDown(
bool& negative, bool& negative,
@@ -122,13 +116,13 @@ public:
// Modify the result to the correctly rounded value // Modify the result to the correctly rounded value
void void
doRound(rep& drops, std::string location); doRound(rep& drops, std::string_view location);
private: private:
void void
doPush(unsigned d) noexcept; doPush(unsigned d) noexcept;
template <UnsignedMantissa T> template <detail::UnsignedMantissa T>
void void
bringIntoRange( bringIntoRange(
bool& negative, bool& negative,
@@ -219,7 +213,7 @@ Number::Guard::round() noexcept
return 0; return 0;
} }
template <UnsignedMantissa T> template <detail::UnsignedMantissa T>
void void
Number::Guard::bringIntoRange( Number::Guard::bringIntoRange(
bool& negative, bool& negative,
@@ -238,13 +232,13 @@ Number::Guard::bringIntoRange(
{ {
constexpr Number zero = Number{}; constexpr Number zero = Number{};
negative = zero.negative_; negative = false;
mantissa = zero.mantissa_; mantissa = zero.mantissa_;
exponent = zero.exponent_; exponent = zero.exponent_;
} }
} }
template <UnsignedMantissa T> template <detail::UnsignedMantissa T>
void void
Number::Guard::doRoundUp( Number::Guard::doRoundUp(
bool& negative, bool& negative,
@@ -252,7 +246,7 @@ Number::Guard::doRoundUp(
int& exponent, int& exponent,
internalrep const& minMantissa, internalrep const& minMantissa,
internalrep const& maxMantissa, internalrep const& maxMantissa,
std::string location) std::string_view location)
{ {
auto r = round(); auto r = round();
if (r == 1 || (r == 0 && (mantissa & 1) == 1)) if (r == 1 || (r == 0 && (mantissa & 1) == 1))
@@ -260,7 +254,7 @@ Number::Guard::doRoundUp(
++mantissa; ++mantissa;
// Ensure mantissa after incrementing fits within both the // Ensure mantissa after incrementing fits within both the
// min/maxMantissa range and is a valid "rep". // min/maxMantissa range and is a valid "rep".
if (mantissa > maxMantissa || mantissa > maxRep) if (mantissa > maxMantissa)
{ {
mantissa /= 10; mantissa /= 10;
++exponent; ++exponent;
@@ -268,10 +262,10 @@ Number::Guard::doRoundUp(
} }
bringIntoRange(negative, mantissa, exponent, minMantissa); bringIntoRange(negative, mantissa, exponent, minMantissa);
if (exponent > maxExponent) if (exponent > maxExponent)
throw std::overflow_error(location); throw std::overflow_error(std::string{location});
} }
template <UnsignedMantissa T> template <detail::UnsignedMantissa T>
void void
Number::Guard::doRoundDown( Number::Guard::doRoundDown(
bool& negative, bool& negative,
@@ -294,12 +288,13 @@ Number::Guard::doRoundDown(
// Modify the result to the correctly rounded value // Modify the result to the correctly rounded value
void void
Number::Guard::doRound(rep& drops, std::string location) Number::Guard::doRound(rep& drops, std::string_view location)
{ {
auto r = round(); auto r = round();
if (r == 1 || (r == 0 && (drops & 1) == 1)) if (r == 1 || (r == 0 && (drops & 1) == 1))
{ {
if (drops >= maxRep) auto const& range = range_.get();
if (drops >= range.max)
{ {
static_assert(sizeof(internalrep) == sizeof(rep)); static_assert(sizeof(internalrep) == sizeof(rep));
// This should be impossible, because it's impossible to represent // This should be impossible, because it's impossible to represent
@@ -308,7 +303,7 @@ Number::Guard::doRound(rep& drops, std::string location)
// or "(maxRep + 1) / 10", neither of which will round up when // or "(maxRep + 1) / 10", neither of which will round up when
// converting to rep, though the latter might overflow _before_ // converting to rep, though the latter might overflow _before_
// rounding. // rounding.
throw std::overflow_error(location); // LCOV_EXCL_LINE throw std::overflow_error(std::string{location}); // LCOV_EXCL_LINE
} }
++drops; ++drops;
} }
@@ -328,17 +323,134 @@ Number::externalToInternal(rep mantissa)
// If the mantissa is already positive, just return it // If the mantissa is already positive, just return it
if (mantissa >= 0) if (mantissa >= 0)
return mantissa; return mantissa;
// If the mantissa is negative, but fits within the positive range of rep,
// return it negated
if (mantissa >= -std::numeric_limits<rep>::max())
return -mantissa;
// If the mantissa doesn't fit within the positive range, convert to // Cast to unsigned before negating to avoid undefined behavior
// int128_t, negate that, and cast it back down to the internalrep // when v == INT64_MIN (negating INT64_MIN in signed is UB)
// In practice, this is only going to cover the case of return -static_cast<internalrep>(mantissa);
// std::numeric_limits<rep>::min(). }
int128_t temp = mantissa;
return static_cast<internalrep>(-temp); /** Breaks down the number into components, potentially de-normalizing it.
*
* Ensures that the mantissa always has range_.log digits.
*
*/
template <detail::UnsignedMantissa Rep>
std::tuple<bool, Rep, int>
Number::toInternal(MantissaRange const& range) const
{
auto exponent = exponent_;
bool const negative = mantissa_ < 0;
auto const sign = negative ? -1 : 1;
Rep mantissa = static_cast<Rep>(sign * mantissa_);
auto const referenceMin = range.referenceMin;
auto const minMantissa = range.min;
if (mantissa != 0 && mantissa >= minMantissa && mantissa < referenceMin)
{
// Ensure the mantissa has the correct number of digits
mantissa *= 10;
--exponent;
XRPL_ASSERT_PARTS(
mantissa >= referenceMin && mantissa < referenceMin * 10,
"ripple::Number::toInternal()",
"Number is within reference range and has 'log' digits");
}
return {negative, mantissa, exponent};
}
/** Breaks down the number into components, potentially de-normalizing it.
*
* Ensures that the mantissa always has range_.log digits.
*
*/
template <detail::UnsignedMantissa Rep>
std::tuple<bool, Rep, int>
Number::toInternal() const
{
return toInternal(range_);
}
/** Rebuilds the number from components.
*
* If "normalized" is true, the values are expected to be normalized - all
* in their valid ranges.
*
* If "normalized" is false, the values are expected to be "near
* normalized", meaning that the mantissa has to be modified at most once to
* bring it back into range.
*
*/
template <bool expectNormal, detail::UnsignedMantissa Rep>
void
Number::fromInternal(
bool negative,
Rep mantissa,
int exponent,
MantissaRange const* pRange)
{
if constexpr (std::is_same_v<
std::bool_constant<expectNormal>,
std::false_type>)
{
if (!pRange)
throw std::runtime_error("Missing range to Number::fromInternal!");
auto const& range = *pRange;
auto const maxMantissa = range.max;
auto const minMantissa = range.min;
XRPL_ASSERT_PARTS(
mantissa >= minMantissa,
"ripple::Number::fromInternal",
"mantissa large enough");
if (mantissa > maxMantissa || mantissa < minMantissa)
{
normalize(negative, mantissa, exponent, range.min, maxMantissa);
}
XRPL_ASSERT_PARTS(
mantissa >= minMantissa && mantissa <= maxMantissa,
"ripple::Number::fromInternal",
"mantissa in range");
}
auto const sign = negative ? -1 : 1;
mantissa_ = sign * static_cast<rep>(mantissa);
exponent_ = exponent;
XRPL_ASSERT_PARTS(
(pRange && isnormal(*pRange)) || isnormal(),
"ripple::Number::fromInternal",
"Number is normalized");
}
/** Rebuilds the number from components.
*
* If "normalized" is true, the values are expected to be normalized - all in
* their valid ranges.
*
* If "normalized" is false, the values are expected to be "near normalized",
* meaning that the mantissa has to be modified at most once to bring it back
* into range.
*
*/
template <bool expectNormal, detail::UnsignedMantissa Rep>
void
Number::fromInternal(bool negative, Rep mantissa, int exponent)
{
MantissaRange const* pRange = nullptr;
if constexpr (std::is_same_v<
std::bool_constant<expectNormal>,
std::false_type>)
{
pRange = &Number::range_.get();
}
fromInternal(negative, mantissa, exponent, pRange);
} }
constexpr Number constexpr Number
@@ -346,7 +458,7 @@ Number::oneSmall()
{ {
return Number{ return Number{
false, false,
Number::smallRange.min, Number::smallRange.referenceMin,
-Number::smallRange.log, -Number::smallRange.log,
Number::unchecked{}}; Number::unchecked{}};
}; };
@@ -358,7 +470,7 @@ Number::oneLarge()
{ {
return Number{ return Number{
false, false,
Number::largeRange.min, Number::largeRange.referenceMin,
-Number::largeRange.log, -Number::largeRange.log,
Number::unchecked{}}; Number::unchecked{}};
}; };
@@ -366,106 +478,91 @@ Number::oneLarge()
constexpr Number oneLrg = Number::oneLarge(); constexpr Number oneLrg = Number::oneLarge();
Number Number
Number::one() Number::one(MantissaRange const& range)
{ {
if (&range_.get() == &smallRange) if (&range == &smallRange)
return oneSml; return oneSml;
XRPL_ASSERT(&range_.get() == &largeRange, "Number::one() : valid range_"); XRPL_ASSERT(&range == &largeRange, "Number::one() : valid range");
return oneLrg; return oneLrg;
} }
Number
Number::one()
{
return one(range_);
}
// Use the member names in this static function for now so the diff is cleaner // 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 <class T> template <class T>
void void
doNormalize( doNormalize(
bool& negative, bool& negative,
T& mantissa_, T& mantissa,
int& exponent_, int& exponent,
MantissaRange::rep const& minMantissa, MantissaRange::rep const& minMantissa,
MantissaRange::rep const& maxMantissa) MantissaRange::rep const& maxMantissa)
{ {
auto constexpr minExponent = Number::minExponent; auto constexpr minExponent = Number::minExponent;
auto constexpr maxExponent = Number::maxExponent; auto constexpr maxExponent = Number::maxExponent;
auto constexpr maxRep = Number::maxRep;
using Guard = Number::Guard; using Guard = Number::Guard;
constexpr Number zero = Number{}; constexpr Number zero = Number{};
if (mantissa_ == 0) if (mantissa == 0 || (mantissa < minMantissa && exponent <= minExponent))
{ {
mantissa_ = zero.mantissa_; mantissa = zero.mantissa_;
exponent_ = zero.exponent_; exponent = zero.exponent_;
negative = zero.negative_; negative = false;
return; return;
} }
auto m = mantissa_;
while ((m < minMantissa) && (exponent_ > minExponent)) auto m = mantissa;
while ((m < minMantissa) && (exponent > minExponent))
{ {
m *= 10; m *= 10;
--exponent_; --exponent;
} }
Guard g; Guard g;
if (negative) if (negative)
g.set_negative(); g.set_negative();
while (m > maxMantissa) while (m > maxMantissa)
{ {
if (exponent_ >= maxExponent) if (exponent >= maxExponent)
throw std::overflow_error("Number::normalize 1"); throw std::overflow_error("Number::normalize 1");
g.push(m % 10); g.push(m % 10);
m /= 10; m /= 10;
++exponent_; ++exponent;
} }
if ((exponent_ < minExponent) || (m < minMantissa)) if ((exponent < minExponent) || (m == 0))
{ {
mantissa_ = zero.mantissa_; mantissa = zero.mantissa_;
exponent_ = zero.exponent_; exponent = zero.exponent_;
negative = zero.negative_; negative = false;
return; return;
} }
// 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( XRPL_ASSERT_PARTS(
m <= maxRep, m <= maxMantissa,
"xrpl::doNormalize", "xrpl::doNormalize",
"intermediate mantissa fits in int64"); "intermediate mantissa fits in int64");
mantissa_ = m; mantissa = m;
g.doRoundUp( g.doRoundUp(
negative, negative,
mantissa_, mantissa,
exponent_, exponent,
minMantissa, minMantissa,
maxMantissa, maxMantissa,
"Number::normalize 2"); "Number::normalize 2");
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
mantissa_ >= minMantissa && mantissa_ <= maxMantissa, mantissa >= minMantissa && mantissa <= maxMantissa,
"xrpl::doNormalize", "xrpl::doNormalize",
"final mantissa fits in range"); "final mantissa fits in range");
XRPL_ASSERT_PARTS(
exponent >= minExponent && exponent <= maxExponent,
"xrpl::doNormalize",
"final exponent fits in range");
} }
template <> template <>
@@ -504,11 +601,20 @@ Number::normalize<unsigned long>(
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa); doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa);
} }
void
Number::normalize(MantissaRange const& range)
{
auto [negative, mantissa, exponent] = toInternal(range);
normalize(negative, mantissa, exponent, range.min, range.max);
fromInternal(negative, mantissa, exponent, &range);
}
void void
Number::normalize() Number::normalize()
{ {
auto const& range = range_.get(); normalize(range_);
normalize(negative_, mantissa_, exponent_, range.min, range.max);
} }
// Copy the number, but set a new exponent. Because the mantissa doesn't change, // Copy the number, but set a new exponent. Because the mantissa doesn't change,
@@ -518,24 +624,33 @@ Number
Number::shiftExponent(int exponentDelta) const Number::shiftExponent(int exponentDelta) const
{ {
XRPL_ASSERT_PARTS(isnormal(), "xrpl::Number::shiftExponent", "normalized"); XRPL_ASSERT_PARTS(isnormal(), "xrpl::Number::shiftExponent", "normalized");
auto const newExponent = exponent_ + exponentDelta;
if (newExponent >= maxExponent) Number result = *this;
result.exponent_ += exponentDelta;
if (result.exponent_ >= maxExponent)
throw std::overflow_error("Number::shiftExponent"); throw std::overflow_error("Number::shiftExponent");
if (newExponent < minExponent) if (result.exponent_ < minExponent)
{ {
return Number{}; return Number{};
} }
Number const result{negative_, mantissa_, newExponent, unchecked{}};
XRPL_ASSERT_PARTS(
result.isnormal(),
"xrpl::Number::shiftExponent",
"result is normalized");
return result; return result;
} }
Number::Number(bool negative, internalrep mantissa, int exponent, normalized)
{
auto const& range = range_.get();
normalize(negative, mantissa, exponent, range.min, range.max);
fromInternal(negative, mantissa, exponent, &range);
}
Number& Number&
Number::operator+=(Number const& y) Number::operator+=(Number const& y)
{ {
auto const& range = range_.get();
constexpr Number zero = Number{}; constexpr Number zero = Number{};
if (y == zero) if (y == zero)
return *this; return *this;
@@ -551,7 +666,7 @@ Number::operator+=(Number const& y)
} }
XRPL_ASSERT( XRPL_ASSERT(
isnormal() && y.isnormal(), isnormal(range) && y.isnormal(range),
"xrpl::Number::operator+=(Number) : is normal"); "xrpl::Number::operator+=(Number) : is normal");
// *n = negative // *n = negative
// *s = sign // *s = sign
@@ -560,13 +675,10 @@ Number::operator+=(Number const& y)
// Need to use uint128_t, because large mantissas can overflow when added // Need to use uint128_t, because large mantissas can overflow when added
// together. // together.
bool xn = negative_; auto [xn, xm, xe] = toInternal<uint128_t>(range);
uint128_t xm = mantissa_;
auto xe = exponent_; auto [yn, ym, ye] = y.toInternal<uint128_t>(range);
bool yn = y.negative_;
uint128_t ym = y.mantissa_;
auto ye = y.exponent_;
Guard g; Guard g;
if (xe < ye) if (xe < ye)
{ {
@@ -591,14 +703,13 @@ Number::operator+=(Number const& y)
} while (xe > ye); } while (xe > ye);
} }
auto const& range = range_.get();
auto const& minMantissa = range.min; auto const& minMantissa = range.min;
auto const& maxMantissa = range.max; auto const& maxMantissa = range.max;
if (xn == yn) if (xn == yn)
{ {
xm += ym; xm += ym;
if (xm > maxMantissa || xm > maxRep) if (xm > maxMantissa)
{ {
g.push(xm % 10); g.push(xm % 10);
xm /= 10; xm /= 10;
@@ -619,7 +730,7 @@ Number::operator+=(Number const& y)
xe = ye; xe = ye;
xn = yn; xn = yn;
} }
while (xm < minMantissa && xm * 10 <= maxRep) while (xm < minMantissa)
{ {
xm *= 10; xm *= 10;
xm -= g.pop(); xm -= g.pop();
@@ -628,10 +739,8 @@ Number::operator+=(Number const& y)
g.doRoundDown(xn, xm, xe, minMantissa); g.doRoundDown(xn, xm, xe, minMantissa);
} }
negative_ = xn; normalize(xn, xm, xe, minMantissa, maxMantissa);
mantissa_ = static_cast<internalrep>(xm); fromInternal(xn, xm, xe, &range);
exponent_ = xe;
normalize();
return *this; return *this;
} }
@@ -666,6 +775,8 @@ divu10(uint128_t& u)
Number& Number&
Number::operator*=(Number const& y) Number::operator*=(Number const& y)
{ {
auto const& range = range_.get();
constexpr Number zero = Number{}; constexpr Number zero = Number{};
if (*this == zero) if (*this == zero)
return *this; return *this;
@@ -679,15 +790,11 @@ Number::operator*=(Number const& y)
// *m = mantissa // *m = mantissa
// *e = exponent // *e = exponent
bool xn = negative_; auto [xn, xm, xe] = toInternal(range);
int xs = xn ? -1 : 1; int xs = xn ? -1 : 1;
internalrep xm = mantissa_;
auto xe = exponent_;
bool yn = y.negative_; auto [yn, ym, ye] = y.toInternal(range);
int ys = yn ? -1 : 1; int ys = yn ? -1 : 1;
internalrep ym = y.mantissa_;
auto ye = y.exponent_;
auto zm = uint128_t(xm) * uint128_t(ym); auto zm = uint128_t(xm) * uint128_t(ym);
auto ze = xe + ye; auto ze = xe + ye;
@@ -697,11 +804,10 @@ Number::operator*=(Number const& y)
if (zn) if (zn)
g.set_negative(); g.set_negative();
auto const& range = range_.get();
auto const& minMantissa = range.min; auto const& minMantissa = range.min;
auto const& maxMantissa = range.max; auto const& maxMantissa = range.max;
while (zm > maxMantissa || zm > maxRep) while (zm > maxMantissa)
{ {
// The following is optimization for: // The following is optimization for:
// g.push(static_cast<unsigned>(zm % 10)); // g.push(static_cast<unsigned>(zm % 10));
@@ -718,17 +824,17 @@ Number::operator*=(Number const& y)
minMantissa, minMantissa,
maxMantissa, maxMantissa,
"Number::multiplication overflow : exponent is " + std::to_string(xe)); "Number::multiplication overflow : exponent is " + std::to_string(xe));
negative_ = zn;
mantissa_ = xm;
exponent_ = xe;
normalize(); normalize(zn, xm, xe, minMantissa, maxMantissa);
fromInternal(zn, xm, xe, &range);
return *this; return *this;
} }
Number& Number&
Number::operator/=(Number const& y) Number::operator/=(Number const& y)
{ {
auto const& range = range_.get();
constexpr Number zero = Number{}; constexpr Number zero = Number{};
if (y == zero) if (y == zero)
throw std::overflow_error("Number: divide by 0"); throw std::overflow_error("Number: divide by 0");
@@ -741,17 +847,12 @@ Number::operator/=(Number const& y)
// *m = mantissa // *m = mantissa
// *e = exponent // *e = exponent
bool np = negative_; auto [np, nm, ne] = toInternal(range);
int ns = (np ? -1 : 1); int ns = (np ? -1 : 1);
auto nm = mantissa_;
auto ne = exponent_;
bool dp = y.negative_; auto [dp, dm, de] = y.toInternal(range);
int ds = (dp ? -1 : 1); 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& minMantissa = range.min;
auto const& maxMantissa = range.max; auto const& maxMantissa = range.max;
@@ -763,7 +864,7 @@ Number::operator/=(Number const& y)
// f can be up to 10^(38-19) = 10^19 safely // f can be up to 10^(38-19) = 10^19 safely
static_assert(smallRange.log == 15); static_assert(smallRange.log == 15);
static_assert(largeRange.log == 18); static_assert(largeRange.log == 18);
bool small = Number::getMantissaScale() == MantissaRange::small; bool small = range.scale == MantissaRange::small;
uint128_t const f = uint128_t const f =
small ? 100'000'000'000'000'000 : 10'000'000'000'000'000'000ULL; small ? 100'000'000'000'000'000 : 10'000'000'000'000'000'000ULL;
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
@@ -815,11 +916,9 @@ Number::operator/=(Number const& y)
} }
} }
normalize(zn, zm, ze, minMantissa, maxMantissa); normalize(zn, zm, ze, minMantissa, maxMantissa);
negative_ = zn; fromInternal(zn, zm, ze, &range);
mantissa_ = static_cast<internalrep>(zm);
exponent_ = ze;
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
isnormal(), "xrpl::Number::operator/=", "result is normalized"); isnormal(range), "xrpl::Number::operator/=", "result is normalized");
return *this; return *this;
} }
@@ -831,7 +930,7 @@ Number::operator rep() const
Guard g; Guard g;
if (drops != 0) if (drops != 0)
{ {
if (negative_) if (drops < 0)
{ {
g.set_negative(); g.set_negative();
drops = -drops; drops = -drops;
@@ -843,7 +942,7 @@ Number::operator rep() const
} }
for (; offset > 0; --offset) for (; offset > 0; --offset)
{ {
if (drops > maxRep / 10) if (drops >= largeRange.min)
throw std::overflow_error("Number::operator rep() overflow"); throw std::overflow_error("Number::operator rep() overflow");
drops *= 10; drops *= 10;
} }
@@ -873,20 +972,22 @@ Number::truncate() const noexcept
std::string std::string
to_string(Number const& amount) to_string(Number const& amount)
{ {
auto const& range = Number::range_.get();
// keep full internal accuracy, but make more human friendly if possible // keep full internal accuracy, but make more human friendly if possible
constexpr Number zero = Number{}; constexpr Number zero = Number{};
if (amount == zero) if (amount == zero)
return "0"; return "0";
auto exponent = amount.exponent_; // The mantissa must have a set number of decimal places for this to work
auto mantissa = amount.mantissa_; auto [negative, mantissa, exponent] = amount.toInternal(range);
bool const negative = amount.negative_;
// Use scientific notation for exponents that are too small or too large // Use scientific notation for exponents that are too small or too large
auto const rangeLog = Number::mantissaLog(); auto const rangeLog = range.log;
if (((exponent != 0) && if (((exponent != 0 && amount.exponent() != 0) &&
((exponent < -(rangeLog + 10)) || (exponent > -(rangeLog - 10))))) ((exponent < -(rangeLog + 10)) || (exponent > -(rangeLog - 10)))))
{ {
// Remove trailing zeroes from the mantissa.
while (mantissa != 0 && mantissa % 10 == 0 && while (mantissa != 0 && mantissa % 10 == 0 &&
exponent < Number::maxExponent) exponent < Number::maxExponent)
{ {
@@ -895,8 +996,11 @@ to_string(Number const& amount)
} }
std::string ret = negative ? "-" : ""; std::string ret = negative ? "-" : "";
ret.append(std::to_string(mantissa)); ret.append(std::to_string(mantissa));
ret.append(1, 'e'); if (exponent != 0)
ret.append(std::to_string(exponent)); {
ret.append(1, 'e');
ret.append(std::to_string(exponent));
}
return ret; return ret;
} }
@@ -984,20 +1088,11 @@ power(Number const& f, unsigned n)
return r; return r;
} }
// Returns f^(1/d)
// Uses NewtonRaphson iterations until the result stops changing
// to find the non-negative root of the polynomial g(x) = x^d - f
// This function, and power(Number f, unsigned n, unsigned d)
// treat corner cases such as 0 roots as advised by Annex F of
// the C standard, which itself is consistent with the IEEE
// floating point standards.
Number Number
root(Number f, unsigned d) Number::root(MantissaRange const& range, Number f, unsigned d)
{ {
constexpr Number zero = Number{}; constexpr Number zero = Number{};
auto const one = Number::one(); auto const one = Number::one(range);
if (f == one || d == 1) if (f == one || d == 1)
return f; return f;
@@ -1014,22 +1109,30 @@ root(Number f, unsigned d)
if (f == zero) if (f == zero)
return f; return f;
// Scale f into the range (0, 1) such that f's exponent is a multiple of d auto const [e, di] = [&]() {
auto e = f.exponent_ + Number::mantissaLog() + 1; auto const [negative, mantissa, exponent] = f.toInternal(range);
auto const di = static_cast<int>(d);
auto ex = [e = e, di = di]() // Euclidean remainder of e/d // Scale f into the range (0, 1) such that the scale change (e) is a
{ // multiple of the root (d)
int k = (e >= 0 ? e : e - (di - 1)) / di; auto e = exponent + range.log + 1;
int k2 = e - k * di; auto const di = static_cast<int>(d);
if (k2 == 0) auto ex = [e = e, di = di]() // Euclidean remainder of e/d
return 0; {
return di - k2; int k = (e >= 0 ? e : e - (di - 1)) / di;
int k2 = e - k * di;
if (k2 == 0)
return 0;
return di - k2;
}();
e += ex;
f = f.shiftExponent(-e); // f /= 10^e;
return std::make_tuple(e, di);
}(); }();
e += ex;
f = f.shiftExponent(-e); // f /= 10^e;
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
f.isnormal(), "xrpl::root(Number, unsigned)", "f is normalized"); e % di == 0, "xrpl::root(Number, unsigned)", "e is divisible by d");
XRPL_ASSERT_PARTS(
f.isnormal(range), "xrpl::root(Number, unsigned)", "f is normalized");
bool neg = false; bool neg = false;
if (f < zero) if (f < zero)
{ {
@@ -1063,17 +1166,34 @@ root(Number f, unsigned d)
// return r * 10^(e/d) to reverse scaling // return r * 10^(e/d) to reverse scaling
auto const result = r.shiftExponent(e / di); auto const result = r.shiftExponent(e / di);
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
result.isnormal(), result.isnormal(range),
"xrpl::root(Number, unsigned)", "xrpl::root(Number, unsigned)",
"result is normalized"); "result is normalized");
return result; return result;
} }
// Returns f^(1/d)
// Uses NewtonRaphson iterations until the result stops changing
// to find the non-negative root of the polynomial g(x) = x^d - f
// This function, and power(Number f, unsigned n, unsigned d)
// treat corner cases such as 0 roots as advised by Annex F of
// the C standard, which itself is consistent with the IEEE
// floating point standards.
Number
root(Number f, unsigned d)
{
auto const& range = Number::range_.get();
return Number::root(range, f, d);
}
Number Number
root2(Number f) root2(Number f)
{ {
auto const& range = Number::range_.get();
constexpr Number zero = Number{}; constexpr Number zero = Number{};
auto const one = Number::one(); auto const one = Number::one(range);
if (f == one) if (f == one)
return f; return f;
@@ -1082,12 +1202,19 @@ root2(Number f)
if (f == zero) if (f == zero)
return f; return f;
// Scale f into the range (0, 1) such that f's exponent is a multiple of d auto const e = [&]() {
auto e = f.exponent_ + Number::mantissaLog() + 1; auto const [negative, mantissa, exponent] = f.toInternal(range);
if (e % 2 != 0)
++e; // Scale f into the range (0, 1) such that f's exponent is a
f = f.shiftExponent(-e); // f /= 10^e; // multiple of d
XRPL_ASSERT_PARTS(f.isnormal(), "xrpl::root2(Number)", "f is normalized"); auto e = exponent + range.log + 1;
if (e % 2 != 0)
++e;
f = f.shiftExponent(-e); // f /= 10^e;
return e;
}();
XRPL_ASSERT_PARTS(
f.isnormal(range), "xrpl::root2(Number)", "f is normalized");
// Quadratic least squares curve fit of f^(1/d) in the range [0, 1] // Quadratic least squares curve fit of f^(1/d) in the range [0, 1]
auto const D = 105; auto const D = 105;
@@ -1110,7 +1237,7 @@ root2(Number f)
// return r * 10^(e/2) to reverse scaling // return r * 10^(e/2) to reverse scaling
auto const result = r.shiftExponent(e / 2); auto const result = r.shiftExponent(e / 2);
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
result.isnormal(), "xrpl::root2(Number)", "result is normalized"); result.isnormal(range), "xrpl::root2(Number)", "result is normalized");
return result; return result;
} }
@@ -1120,8 +1247,10 @@ root2(Number f)
Number Number
power(Number const& f, unsigned n, unsigned d) power(Number const& f, unsigned n, unsigned d)
{ {
auto const& range = Number::range_.get();
constexpr Number zero = Number{}; constexpr Number zero = Number{};
auto const one = Number::one(); auto const one = Number::one(range);
if (f == one) if (f == one)
return f; return f;
@@ -1143,7 +1272,7 @@ power(Number const& f, unsigned n, unsigned d)
d /= g; d /= g;
if ((n % 2) == 1 && (d % 2) == 0 && f < zero) if ((n % 2) == 1 && (d % 2) == 0 && f < zero)
throw std::overflow_error("Number::power nan"); throw std::overflow_error("Number::power nan");
return root(power(f, n), d); return Number::root(range, power(f, n), d);
} }
} // namespace xrpl } // namespace xrpl

View File

@@ -4,6 +4,7 @@
#include <xrpld/app/tx/apply.h> #include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/ApplyContext.h> #include <xrpld/app/tx/detail/ApplyContext.h>
#include <xrpld/app/tx/detail/InvariantCheck.h>
#include <xrpl/beast/unit_test/suite.h> #include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h> #include <xrpl/beast/utility/Journal.h>
@@ -20,6 +21,9 @@
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <initializer_list>
#include <string>
namespace xrpl { namespace xrpl {
namespace test { namespace test {
@@ -3888,6 +3892,140 @@ class Invariants_test : public beast::unit_test::suite
precloseMpt); precloseMpt);
} }
void
testVaultComputeMinScale()
{
using namespace jtx;
Account const issuer{"issuer"};
PrettyAsset const vaultAsset = issuer["IOU"];
struct TestCase
{
std::string name;
std::int32_t expectedMinScale;
std::initializer_list<ValidVault::DeltaInfo const> values;
};
NumberMantissaScaleGuard g{MantissaRange::large};
auto makeDelta =
[&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
return {n, n.scale<STAmount>(vaultAsset.raw())};
};
auto const testCases = std::vector<TestCase>{
{
.name = "No values",
.expectedMinScale = 0,
.values = {},
},
{
.name = "Mixed integer and Number values",
.expectedMinScale = -15,
.values =
{makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})},
},
{
.name = "Mixed scales",
.expectedMinScale = -17,
.values =
{makeDelta(Number{1, -2}),
makeDelta(Number{5, -3}),
makeDelta(Number{3, -2})},
},
{
.name = "Equal scales",
.expectedMinScale = -16,
.values =
{makeDelta(Number{1, -1}),
makeDelta(Number{5, -1}),
makeDelta(Number{1, -1})},
},
{
.name = "Mixed mantissa sizes",
.expectedMinScale = -12,
.values =
{makeDelta(Number{1}),
makeDelta(Number{1234, -3}),
makeDelta(Number{12345, -6}),
makeDelta(Number{123, 1})},
},
};
for (auto const& tc : testCases)
{
testcase("vault computeMinScale: " + tc.name);
auto const actualScale =
ValidVault::computeMinScale(vaultAsset, tc.values);
BEAST_EXPECTS(
actualScale == tc.expectedMinScale,
"expected: " + std::to_string(tc.expectedMinScale) +
", actual: " + std::to_string(actualScale));
for (auto const& num : tc.values)
{
// None of these scales are far enough apart that rounding the
// values would lose information, so check that the rounded
// value matches the original.
auto const actualRounded =
roundToAsset(vaultAsset, num.delta, actualScale);
BEAST_EXPECTS(
actualRounded == num.delta,
"number " + to_string(num.delta) + " rounded to scale " +
std::to_string(actualScale) + " is " +
to_string(actualRounded));
}
}
auto const testCases2 = std::vector<TestCase>{
{
.name = "False equivalence",
.expectedMinScale = -15,
.values =
{
makeDelta(Number{1234567890123456789, -18}),
makeDelta(Number{12345, -4}),
makeDelta(Number{1}),
},
},
};
// Unlike the first set of test cases, the values in these test could
// look equivalent if using the wrong scale.
for (auto const& tc : testCases2)
{
testcase("vault computeMinScale: " + tc.name);
auto const actualScale =
ValidVault::computeMinScale(vaultAsset, tc.values);
BEAST_EXPECTS(
actualScale == tc.expectedMinScale,
"expected: " + std::to_string(tc.expectedMinScale) +
", actual: " + std::to_string(actualScale));
std::optional<Number> first;
Number firstRounded;
for (auto const& num : tc.values)
{
if (!first)
{
first = num.delta;
firstRounded =
roundToAsset(vaultAsset, num.delta, actualScale);
continue;
}
auto const numRounded =
roundToAsset(vaultAsset, num.delta, actualScale);
BEAST_EXPECTS(
numRounded != firstRounded,
"at a scale of " + std::to_string(actualScale) + " " +
to_string(num.delta) + " == " + to_string(*first));
}
}
}
public: public:
void void
run() override run() override
@@ -3911,6 +4049,7 @@ public:
testValidPseudoAccounts(); testValidPseudoAccounts();
testValidLoanBroker(); testValidLoanBroker();
testVault(); testVault();
testVaultComputeMinScale();
} }
}; };

View File

@@ -3,16 +3,11 @@
#include <test/jtx.h> #include <test/jtx.h>
#include <test/jtx/Account.h> #include <test/jtx/Account.h>
#include <test/jtx/amount.h> #include <test/jtx/amount.h>
#include <test/jtx/mpt.h>
#include <xrpld/app/misc/LendingHelpers.h> #include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpld/app/tx/detail/Batch.h> #include <xrpld/app/tx/detail/Batch.h>
#include <xrpld/app/tx/detail/LoanSet.h> #include <xrpld/app/tx/detail/LoanSet.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/protocol/SField.h>
#include <string> #include <string>
#include <vector> #include <vector>

View File

@@ -7641,6 +7641,149 @@ protected:
BEAST_EXPECT(afterSecondCoverAvailable == 0); BEAST_EXPECT(afterSecondCoverAvailable == 0);
} }
// Tests that vault withdrawals work correctly when the vault has unrealized
// loss from an impaired loan, ensuring the invariant check properly
// accounts for the loss.
void
testWithdrawReflectsUnrealizedLoss()
{
using namespace jtx;
using namespace loan;
using namespace std::chrono_literals;
testcase("Vault withdraw reflects sfLossUnrealized");
// Test constants
static constexpr std::int64_t INITIAL_FUNDING = 1'000'000;
static constexpr std::int64_t LENDER_INITIAL_IOU = 5'000'000;
static constexpr std::int64_t DEPOSITOR_INITIAL_IOU = 1'000'000;
static constexpr std::int64_t BORROWER_INITIAL_IOU = 100'000;
static constexpr std::int64_t DEPOSIT_AMOUNT = 5'000;
static constexpr std::int64_t PRINCIPAL_AMOUNT = 99;
static constexpr std::uint64_t EXPECTED_SHARES_PER_DEPOSITOR =
5'000'000'000;
static constexpr std::uint32_t PAYMENT_INTERVAL = 600;
static constexpr std::uint32_t PAYMENT_TOTAL = 2;
Env env(*this, all);
// Setup accounts
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const depositorA{"lpA"};
Account const depositorB{"lpB"};
Account const borrower{"borrowerA"};
env.fund(
XRP(INITIAL_FUNDING),
issuer,
lender,
depositorA,
depositorB,
borrower);
env.close();
// Setup trust lines
PrettyAsset const iouAsset = issuer[iouCurrency];
env(trust(lender, iouAsset(10'000'000)));
env(trust(depositorA, iouAsset(10'000'000)));
env(trust(depositorB, iouAsset(10'000'000)));
env(trust(borrower, iouAsset(10'000'000)));
env.close();
// Fund accounts with IOUs
env(pay(issuer, lender, iouAsset(LENDER_INITIAL_IOU)));
env(pay(issuer, depositorA, iouAsset(DEPOSITOR_INITIAL_IOU)));
env(pay(issuer, depositorB, iouAsset(DEPOSITOR_INITIAL_IOU)));
env(pay(issuer, borrower, iouAsset(BORROWER_INITIAL_IOU)));
env.close();
// Create vault and broker, then add deposits from two depositors
auto const broker = createVaultAndBroker(env, iouAsset, lender);
Vault v{env};
env(v.deposit({
.depositor = depositorA,
.id = broker.vaultKeylet().key,
.amount = iouAsset(DEPOSIT_AMOUNT),
}),
ter(tesSUCCESS));
env(v.deposit({
.depositor = depositorB,
.id = broker.vaultKeylet().key,
.amount = iouAsset(DEPOSIT_AMOUNT),
}),
ter(tesSUCCESS));
env.close();
// Create a loan
auto const sleBroker = env.le(keylet::loanbroker(broker.brokerID));
if (!BEAST_EXPECT(sleBroker))
return;
auto const loanKeylet =
keylet::loan(broker.brokerID, sleBroker->at(sfLoanSequence));
env(set(borrower, broker.brokerID, PRINCIPAL_AMOUNT),
sig(sfCounterpartySignature, lender),
paymentTotal(PAYMENT_TOTAL),
paymentInterval(PAYMENT_INTERVAL),
fee(env.current()->fees().base * 2),
ter(tesSUCCESS));
env.close();
// Impair the loan to create unrealized loss
env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tesSUCCESS));
env.close();
// Verify unrealized loss is recorded in the vault
auto const vaultAfterImpair = env.le(broker.vaultKeylet());
if (!BEAST_EXPECT(vaultAfterImpair))
return;
BEAST_EXPECT(
vaultAfterImpair->at(sfLossUnrealized) ==
broker.asset(PRINCIPAL_AMOUNT).value());
// Helper to get share balance for a depositor
auto const shareAsset = vaultAfterImpair->at(sfShareMPTID);
auto const getShareBalance =
[&](Account const& depositor) -> std::uint64_t {
auto const token =
env.le(keylet::mptoken(shareAsset, depositor.id()));
return token ? token->getFieldU64(sfMPTAmount) : 0;
};
// Verify both depositors have equal shares
auto const sharesLpA = getShareBalance(depositorA);
auto const sharesLpB = getShareBalance(depositorB);
BEAST_EXPECT(sharesLpA == EXPECTED_SHARES_PER_DEPOSITOR);
BEAST_EXPECT(sharesLpB == EXPECTED_SHARES_PER_DEPOSITOR);
BEAST_EXPECT(sharesLpA == sharesLpB);
// Helper to attempt withdrawal
auto const attemptWithdrawShares = [&](Account const& depositor,
std::uint64_t shareAmount,
TER expected) {
STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)};
env(v.withdraw(
{.depositor = depositor,
.id = broker.vaultKeylet().key,
.amount = shareAmt}),
ter(expected));
env.close();
};
// Regression test: Both depositors should successfully withdraw despite
// unrealized loss. Previously failed with invariant violation:
// "withdrawal must change vault and destination balance by equal
// amount". This was caused by sharesToAssetsWithdraw rounding down,
// creating a mismatch where vaultDeltaAssets * -1 != destinationDelta
// when unrealized loss exists.
attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS);
attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
}
public: public:
void void
run() override run() override
@@ -7649,6 +7792,7 @@ public:
testLoanPayLateFullPaymentBypassesPenalties(); testLoanPayLateFullPaymentBypassesPenalties();
testLoanCoverMinimumRoundingExploit(); testLoanCoverMinimumRoundingExploit();
#endif #endif
testWithdrawReflectsUnrealizedLoss();
testInvalidLoanSet(); testInvalidLoanSet();
testCoverDepositWithdrawNonTransferableMPT(); testCoverDepositWithdrawNonTransferableMPT();

View File

@@ -32,9 +32,10 @@ public:
test_limits() test_limits()
{ {
auto const scale = Number::getMantissaScale(); auto const scale = Number::getMantissaScale();
testcase << "test_limits " << to_string(scale);
bool caught = false;
auto const minMantissa = Number::minMantissa(); auto const minMantissa = Number::minMantissa();
testcase << "test_limits " << to_string(scale) << ", " << minMantissa;
bool caught = false;
try try
{ {
Number x = Number x =
@@ -62,8 +63,9 @@ public:
Number{}, Number{},
__LINE__); __LINE__);
test( test(
// Use 1501 to force rounding up
Number{false, minMantissa, 32000, Number::normalized{}} * 1'000 + Number{false, minMantissa, 32000, Number::normalized{}} * 1'000 +
Number{false, 1'500, 32000, Number::normalized{}}, Number{false, 1'501, 32000, Number::normalized{}},
Number{false, minMantissa + 2, 32003, Number::normalized{}}, Number{false, minMantissa + 2, 32003, Number::normalized{}},
__LINE__); __LINE__);
// 9,223,372,036,854,775,808 // 9,223,372,036,854,775,808
@@ -198,12 +200,12 @@ public:
9'999'999'999'999'999'990ULL, 9'999'999'999'999'999'990ULL,
-19, -19,
Number::normalized{}}}, Number::normalized{}}},
{Number{Number::maxRep}, {Number{Number::largestMantissa},
Number{6, -1}, Number{6, -1},
Number{Number::maxRep / 10, 1}}, Number{Number::largestMantissa / 10, 1}},
{Number{Number::maxRep - 1}, {Number{Number::largestMantissa - 1},
Number{1, 0}, Number{1, 0},
Number{Number::maxRep}}, Number{Number::largestMantissa}},
// Test extremes // Test extremes
{ {
// Each Number operand rounds up, so the actual mantissa is // Each Number operand rounds up, so the actual mantissa is
@@ -221,11 +223,30 @@ public:
Number{2, 19}, Number{2, 19},
}, },
{ {
// Does not round. Mantissas are going to be > maxRep, so if // Does not round. Mantissas are going to be >
// added together as uint64_t's, the result will overflow. // largestMantissa, so if added together as uint64_t's, the
// With addition using uint128_t, there's no problem. After // result will overflow. With addition using uint128_t,
// normalizing, the resulting mantissa ends up less than // there's no problem. After normalizing, the resulting
// maxRep. // mantissa ends up less than largestMantissa.
Number{
false,
Number::largestMantissa,
0,
Number::normalized{}},
Number{
false,
Number::largestMantissa,
0,
Number::normalized{}},
Number{
false,
Number::largestMantissa * 2,
0,
Number::normalized{}},
},
{
// These mantissas round down, so adding them together won't
// have any consequences.
Number{ Number{
false, false,
9'999'999'999'999'999'990ULL, 9'999'999'999'999'999'990ULL,
@@ -352,16 +373,24 @@ public:
{Number{1'000'000'000'000'000'001, -18}, {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, -18},
Number{1'000'000'000'000'000'000, -36}}, Number{1'000'000'000'000'000'000, -36}},
{Number{Number::maxRep}, {Number{Number::largestMantissa},
Number{6, -1}, Number{6, -1},
Number{Number::maxRep - 1}}, Number{Number::largestMantissa - 1}},
{Number{false, Number::maxRep + 1, 0, Number::normalized{}}, {Number{
false,
Number::largestMantissa + 1,
0,
Number::normalized{}},
Number{1, 0}, Number{1, 0},
Number{Number::maxRep / 10 + 1, 1}}, Number{Number::largestMantissa / 10 + 1, 1}},
{Number{false, Number::maxRep + 1, 0, Number::normalized{}}, {Number{
false,
Number::largestMantissa + 1,
0,
Number::normalized{}},
Number{3, 0}, Number{3, 0},
Number{Number::maxRep}}, Number{Number::largestMantissa}},
{power(2, 63), Number{3, 0}, Number{Number::maxRep}}, {power(2, 63), Number{3, 0}, Number{Number::largestMantissa}},
}); });
auto test = [this](auto const& c) { auto test = [this](auto const& c) {
for (auto const& [x, y, z] : c) for (auto const& [x, y, z] : c)
@@ -384,14 +413,16 @@ public:
auto const scale = Number::getMantissaScale(); auto const scale = Number::getMantissaScale();
testcase << "test_mul " << to_string(scale); testcase << "test_mul " << to_string(scale);
using Case = std::tuple<Number, Number, Number>; // Case: Factor 1, Factor 2, Expected product, Line number
using Case = std::tuple<Number, Number, Number, int>;
auto test = [this](auto const& c) { auto test = [this](auto const& c) {
for (auto const& [x, y, z] : c) for (auto const& [x, y, z, line] : c)
{ {
auto const result = x * y; auto const result = x * y;
std::stringstream ss; std::stringstream ss;
ss << x << " * " << y << " = " << result << ". Expected: " << z; ss << x << " * " << y << " = " << result << ". Expected: " << z;
BEAST_EXPECTS(result == z, ss.str()); BEAST_EXPECTS(
result == z, ss.str() + " line: " + std::to_string(line));
} }
}; };
auto tests = [&](auto const& cSmall, auto const& cLarge) { auto tests = [&](auto const& cSmall, auto const& cLarge) {
@@ -401,78 +432,105 @@ public:
test(cLarge); test(cLarge);
}; };
auto const maxMantissa = Number::maxMantissa(); auto const maxMantissa = Number::maxMantissa();
auto const maxInternalMantissa =
static_cast<std::uint64_t>(
static_cast<std::int64_t>(power(10, Number::mantissaLog()))) *
10 -
1;
saveNumberRoundMode save{Number::setround(Number::to_nearest)}; saveNumberRoundMode save{Number::setround(Number::to_nearest)};
{ {
auto const cSmall = std::to_array<Case>({ auto const cSmall = std::to_array<Case>({
{Number{7}, Number{8}, Number{56}}, {Number{7}, Number{8}, Number{56}, __LINE__},
{Number{1414213562373095, -15}, {Number{1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{2000000000000000, -15}}, Number{2000000000000000, -15},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{-2000000000000000, -15}}, Number{-2000000000000000, -15},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{-1414213562373095, -15}, Number{-1414213562373095, -15},
Number{2000000000000000, -15}}, Number{2000000000000000, -15},
__LINE__},
{Number{3214285714285706, -15}, {Number{3214285714285706, -15},
Number{3111111111111119, -15}, Number{3111111111111119, -15},
Number{1000000000000000, -14}}, Number{1000000000000000, -14},
__LINE__},
{Number{1000000000000000, -32768}, {Number{1000000000000000, -32768},
Number{1000000000000000, -32768}, Number{1000000000000000, -32768},
Number{0}}, Number{0},
__LINE__},
// Maximum mantissa range // 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'999, 0}, Number{9'999'999'999'999'999, 0},
Number{9'999'999'999'999'998, 16}}, Number{9'999'999'999'999'998, 16},
__LINE__},
}); });
auto const cLarge = std::to_array<Case>({ auto const cLarge = std::to_array<Case>({
// Note that items with extremely large mantissas need to be // Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items // calculated, because otherwise they overflow uint64. Items
// from C with larger mantissa // from C with larger mantissa
{Number{7}, Number{8}, Number{56}}, {Number{7}, Number{8}, Number{56}, __LINE__},
{Number{1414213562373095, -15}, {Number{1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{1999999999999999862, -18}}, Number{1999999999999999862, -18},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{-1999999999999999862, -18}}, Number{-1999999999999999862, -18},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{-1414213562373095, -15}, Number{-1414213562373095, -15},
Number{1999999999999999862, -18}}, Number{1999999999999999862, -18},
__LINE__},
{Number{3214285714285706, -15}, {Number{3214285714285706, -15},
Number{3111111111111119, -15}, Number{3111111111111119, -15},
Number{ Number{
false, false,
9'999'999'999'999'999'579ULL, 9'999'999'999'999'999'579ULL,
-18, -18,
Number::normalized{}}}, Number::normalized{}},
__LINE__},
{Number{1000000000000000000, -32768}, {Number{1000000000000000000, -32768},
Number{1000000000000000000, -32768}, Number{1000000000000000000, -32768},
Number{0}}, Number{0},
__LINE__},
// Items from cSmall expanded for the larger mantissa, // Items from cSmall expanded for the larger mantissa,
// except duplicates. Sadly, it looks like sqrt(2)^2 != 2 // except duplicates. Sadly, it looks like sqrt(2)^2 != 2
// with higher precision // with higher precision
{Number{1414213562373095049, -18}, {Number{1414213562373095049, -18},
Number{1414213562373095049, -18}, Number{1414213562373095049, -18},
Number{2000000000000000001, -18}}, Number{2000000000000000001, -18},
__LINE__},
{Number{-1414213562373095048, -18}, {Number{-1414213562373095048, -18},
Number{1414213562373095048, -18}, Number{1414213562373095048, -18},
Number{-1999999999999999998, -18}}, Number{-1999999999999999998, -18},
__LINE__},
{Number{-1414213562373095048, -18}, {Number{-1414213562373095048, -18},
Number{-1414213562373095049, -18}, Number{-1414213562373095049, -18},
Number{1999999999999999999, -18}}, Number{1999999999999999999, -18},
__LINE__},
{Number{3214285714285714278, -18}, {Number{3214285714285714278, -18},
Number{3111111111111111119, -18}, Number{3111111111111111119, -18},
Number{10, 0}}, Number{10, 0},
// Maximum mantissa range - rounds up to 1e19 __LINE__},
// Maximum internal mantissa range - rounds up to 1e19
{Number{false, maxInternalMantissa, 0, Number::normalized{}},
Number{false, maxInternalMantissa, 0, Number::normalized{}},
Number{1, 38},
__LINE__},
// Maximum actual mantissa range - same as int64 range
{Number{false, maxMantissa, 0, Number::normalized{}}, {Number{false, maxMantissa, 0, Number::normalized{}},
Number{false, maxMantissa, 0, Number::normalized{}}, Number{false, maxMantissa, 0, Number::normalized{}},
Number{1, 38}}, Number{85'070'591'730'234'615'85, 19},
__LINE__},
// Maximum int64 range // Maximum int64 range
{Number{Number::maxRep, 0}, {Number{Number::largestMantissa, 0},
Number{Number::maxRep, 0}, Number{Number::largestMantissa, 0},
Number{85'070'591'730'234'615'85, 19}}, Number{85'070'591'730'234'615'85, 19},
__LINE__},
}); });
tests(cSmall, cLarge); tests(cSmall, cLarge);
} }
@@ -481,76 +539,100 @@ public:
<< " towards_zero"; << " towards_zero";
{ {
auto const cSmall = std::to_array<Case>( auto const cSmall = std::to_array<Case>(
{{Number{7}, Number{8}, Number{56}}, {{Number{7}, Number{8}, Number{56}, __LINE__},
{Number{1414213562373095, -15}, {Number{1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{1999999999999999, -15}}, Number{1999999999999999, -15},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{-1999999999999999, -15}}, Number{-1999999999999999, -15},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{-1414213562373095, -15}, Number{-1414213562373095, -15},
Number{1999999999999999, -15}}, Number{1999999999999999, -15},
__LINE__},
{Number{3214285714285706, -15}, {Number{3214285714285706, -15},
Number{3111111111111119, -15}, Number{3111111111111119, -15},
Number{9999999999999999, -15}}, Number{9999999999999999, -15},
__LINE__},
{Number{1000000000000000, -32768}, {Number{1000000000000000, -32768},
Number{1000000000000000, -32768}, Number{1000000000000000, -32768},
Number{0}}}); Number{0},
__LINE__}});
auto const cLarge = std::to_array<Case>( auto const cLarge = std::to_array<Case>(
// Note that items with extremely large mantissas need to be // Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items // calculated, because otherwise they overflow uint64. Items
// from C with larger mantissa // from C with larger mantissa
{ {
{Number{7}, Number{8}, Number{56}}, {Number{7}, Number{8}, Number{56}, __LINE__},
{Number{1414213562373095, -15}, {Number{1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{1999999999999999861, -18}}, Number{1999999999999999861, -18},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{-1999999999999999861, -18}}, Number{-1999999999999999861, -18},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{-1414213562373095, -15}, Number{-1414213562373095, -15},
Number{1999999999999999861, -18}}, Number{1999999999999999861, -18},
__LINE__},
{Number{3214285714285706, -15}, {Number{3214285714285706, -15},
Number{3111111111111119, -15}, Number{3111111111111119, -15},
Number{ Number{
false, false,
9999999999999999579ULL, 9999999999999999579ULL,
-18, -18,
Number::normalized{}}}, Number::normalized{}},
__LINE__},
{Number{1000000000000000000, -32768}, {Number{1000000000000000000, -32768},
Number{1000000000000000000, -32768}, Number{1000000000000000000, -32768},
Number{0}}, Number{0},
__LINE__},
// Items from cSmall expanded for the larger mantissa, // Items from cSmall expanded for the larger mantissa,
// except duplicates. Sadly, it looks like sqrt(2)^2 != 2 // except duplicates. Sadly, it looks like sqrt(2)^2 != 2
// with higher precision // with higher precision
{Number{1414213562373095049, -18}, {Number{1414213562373095049, -18},
Number{1414213562373095049, -18}, Number{1414213562373095049, -18},
Number{2, 0}}, Number{2, 0},
__LINE__},
{Number{-1414213562373095048, -18}, {Number{-1414213562373095048, -18},
Number{1414213562373095048, -18}, Number{1414213562373095048, -18},
Number{-1999999999999999997, -18}}, Number{-1999999999999999997, -18},
__LINE__},
{Number{-1414213562373095048, -18}, {Number{-1414213562373095048, -18},
Number{-1414213562373095049, -18}, Number{-1414213562373095049, -18},
Number{1999999999999999999, -18}}, Number{1999999999999999999, -18},
__LINE__},
{Number{3214285714285714278, -18}, {Number{3214285714285714278, -18},
Number{3111111111111111119, -18}, Number{3111111111111111119, -18},
Number{10, 0}}, Number{10, 0},
// Maximum mantissa range - rounds down to maxMantissa/10e1 __LINE__},
// Maximum internal mantissa range - rounds down to
// maxMantissa/10e1
// 99'999'999'999'999'999'800'000'000'000'000'000'100 // 99'999'999'999'999'999'800'000'000'000'000'000'100
{Number{false, maxMantissa, 0, Number::normalized{}}, {Number{
Number{false, maxMantissa, 0, Number::normalized{}}, false, maxInternalMantissa, 0, Number::normalized{}},
Number{
false, maxInternalMantissa, 0, Number::normalized{}},
Number{ Number{
false, false,
maxMantissa / 10 - 1, maxInternalMantissa / 10 - 1,
20, 20,
Number::normalized{}}}, Number::normalized{}},
__LINE__},
// Maximum actual mantissa range - same as int64
{Number{false, maxMantissa, 0, Number::normalized{}},
Number{false, maxMantissa, 0, Number::normalized{}},
Number{85'070'591'730'234'615'84, 19},
__LINE__},
// Maximum int64 range // Maximum int64 range
// 85'070'591'730'234'615'847'396'907'784'232'501'249 // 85'070'591'730'234'615'847'396'907'784'232'501'249
{Number{Number::maxRep, 0}, {Number{Number::largestMantissa, 0},
Number{Number::maxRep, 0}, Number{Number::largestMantissa, 0},
Number{85'070'591'730'234'615'84, 19}}, Number{85'070'591'730'234'615'84, 19},
__LINE__},
}); });
tests(cSmall, cLarge); tests(cSmall, cLarge);
} }
@@ -559,76 +641,100 @@ public:
<< " downward"; << " downward";
{ {
auto const cSmall = std::to_array<Case>( auto const cSmall = std::to_array<Case>(
{{Number{7}, Number{8}, Number{56}}, {{Number{7}, Number{8}, Number{56}, __LINE__},
{Number{1414213562373095, -15}, {Number{1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{1999999999999999, -15}}, Number{1999999999999999, -15},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{-2000000000000000, -15}}, Number{-2000000000000000, -15},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{-1414213562373095, -15}, Number{-1414213562373095, -15},
Number{1999999999999999, -15}}, Number{1999999999999999, -15},
__LINE__},
{Number{3214285714285706, -15}, {Number{3214285714285706, -15},
Number{3111111111111119, -15}, Number{3111111111111119, -15},
Number{9999999999999999, -15}}, Number{9999999999999999, -15},
__LINE__},
{Number{1000000000000000, -32768}, {Number{1000000000000000, -32768},
Number{1000000000000000, -32768}, Number{1000000000000000, -32768},
Number{0}}}); Number{0},
__LINE__}});
auto const cLarge = std::to_array<Case>( auto const cLarge = std::to_array<Case>(
// Note that items with extremely large mantissas need to be // Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items // calculated, because otherwise they overflow uint64. Items
// from C with larger mantissa // from C with larger mantissa
{ {
{Number{7}, Number{8}, Number{56}}, {Number{7}, Number{8}, Number{56}, __LINE__},
{Number{1414213562373095, -15}, {Number{1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{1999999999999999861, -18}}, Number{1999999999999999861, -18},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{-1999999999999999862, -18}}, Number{-1999999999999999862, -18},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{-1414213562373095, -15}, Number{-1414213562373095, -15},
Number{1999999999999999861, -18}}, Number{1999999999999999861, -18},
__LINE__},
{Number{3214285714285706, -15}, {Number{3214285714285706, -15},
Number{3111111111111119, -15}, Number{3111111111111119, -15},
Number{ Number{
false, false,
9'999'999'999'999'999'579ULL, 9'999'999'999'999'999'579ULL,
-18, -18,
Number::normalized{}}}, Number::normalized{}},
__LINE__},
{Number{1000000000000000000, -32768}, {Number{1000000000000000000, -32768},
Number{1000000000000000000, -32768}, Number{1000000000000000000, -32768},
Number{0}}, Number{0},
__LINE__},
// Items from cSmall expanded for the larger mantissa, // Items from cSmall expanded for the larger mantissa,
// except duplicates. Sadly, it looks like sqrt(2)^2 != 2 // except duplicates. Sadly, it looks like sqrt(2)^2 != 2
// with higher precision // with higher precision
{Number{1414213562373095049, -18}, {Number{1414213562373095049, -18},
Number{1414213562373095049, -18}, Number{1414213562373095049, -18},
Number{2, 0}}, Number{2, 0},
__LINE__},
{Number{-1414213562373095048, -18}, {Number{-1414213562373095048, -18},
Number{1414213562373095048, -18}, Number{1414213562373095048, -18},
Number{-1999999999999999998, -18}}, Number{-1999999999999999998, -18},
__LINE__},
{Number{-1414213562373095048, -18}, {Number{-1414213562373095048, -18},
Number{-1414213562373095049, -18}, Number{-1414213562373095049, -18},
Number{1999999999999999999, -18}}, Number{1999999999999999999, -18},
__LINE__},
{Number{3214285714285714278, -18}, {Number{3214285714285714278, -18},
Number{3111111111111111119, -18}, Number{3111111111111111119, -18},
Number{10, 0}}, Number{10, 0},
// Maximum mantissa range - rounds down to maxMantissa/10e1 __LINE__},
// Maximum internal mantissa range - rounds down to
// maxMantissa/10-1
// 99'999'999'999'999'999'800'000'000'000'000'000'100 // 99'999'999'999'999'999'800'000'000'000'000'000'100
{Number{false, maxMantissa, 0, Number::normalized{}}, {Number{
Number{false, maxMantissa, 0, Number::normalized{}}, false, maxInternalMantissa, 0, Number::normalized{}},
Number{
false, maxInternalMantissa, 0, Number::normalized{}},
Number{ Number{
false, false,
maxMantissa / 10 - 1, maxInternalMantissa / 10 - 1,
20, 20,
Number::normalized{}}}, Number::normalized{}},
__LINE__},
// Maximum mantissa range - same as int64
{Number{false, maxMantissa, 0, Number::normalized{}},
Number{false, maxMantissa, 0, Number::normalized{}},
Number{85'070'591'730'234'615'84, 19},
__LINE__},
// Maximum int64 range // Maximum int64 range
// 85'070'591'730'234'615'847'396'907'784'232'501'249 // 85'070'591'730'234'615'847'396'907'784'232'501'249
{Number{Number::maxRep, 0}, {Number{Number::largestMantissa, 0},
Number{Number::maxRep, 0}, Number{Number::largestMantissa, 0},
Number{85'070'591'730'234'615'84, 19}}, Number{85'070'591'730'234'615'84, 19},
__LINE__},
}); });
tests(cSmall, cLarge); tests(cSmall, cLarge);
} }
@@ -637,68 +743,91 @@ public:
<< " upward"; << " upward";
{ {
auto const cSmall = std::to_array<Case>( auto const cSmall = std::to_array<Case>(
{{Number{7}, Number{8}, Number{56}}, {{Number{7}, Number{8}, Number{56}, __LINE__},
{Number{1414213562373095, -15}, {Number{1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{2000000000000000, -15}}, Number{2000000000000000, -15},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{-1999999999999999, -15}}, Number{-1999999999999999, -15},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{-1414213562373095, -15}, Number{-1414213562373095, -15},
Number{2000000000000000, -15}}, Number{2000000000000000, -15},
__LINE__},
{Number{3214285714285706, -15}, {Number{3214285714285706, -15},
Number{3111111111111119, -15}, Number{3111111111111119, -15},
Number{1000000000000000, -14}}, Number{1000000000000000, -14},
__LINE__},
{Number{1000000000000000, -32768}, {Number{1000000000000000, -32768},
Number{1000000000000000, -32768}, Number{1000000000000000, -32768},
Number{0}}}); Number{0},
__LINE__}});
auto const cLarge = std::to_array<Case>( auto const cLarge = std::to_array<Case>(
// Note that items with extremely large mantissas need to be // Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items // calculated, because otherwise they overflow uint64. Items
// from C with larger mantissa // from C with larger mantissa
{ {
{Number{7}, Number{8}, Number{56}}, {Number{7}, Number{8}, Number{56}, __LINE__},
{Number{1414213562373095, -15}, {Number{1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{1999999999999999862, -18}}, Number{1999999999999999862, -18},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{1414213562373095, -15}, Number{1414213562373095, -15},
Number{-1999999999999999861, -18}}, Number{-1999999999999999861, -18},
__LINE__},
{Number{-1414213562373095, -15}, {Number{-1414213562373095, -15},
Number{-1414213562373095, -15}, Number{-1414213562373095, -15},
Number{1999999999999999862, -18}}, Number{1999999999999999862, -18},
__LINE__},
{Number{3214285714285706, -15}, {Number{3214285714285706, -15},
Number{3111111111111119, -15}, Number{3111111111111119, -15},
Number{999999999999999958, -17}}, Number{999999999999999958, -17},
__LINE__},
{Number{1000000000000000000, -32768}, {Number{1000000000000000000, -32768},
Number{1000000000000000000, -32768}, Number{1000000000000000000, -32768},
Number{0}}, Number{0},
__LINE__},
// Items from cSmall expanded for the larger mantissa, // Items from cSmall expanded for the larger mantissa,
// except duplicates. Sadly, it looks like sqrt(2)^2 != 2 // except duplicates. Sadly, it looks like sqrt(2)^2 != 2
// with higher precision // with higher precision
{Number{1414213562373095049, -18}, {Number{1414213562373095049, -18},
Number{1414213562373095049, -18}, Number{1414213562373095049, -18},
Number{2000000000000000001, -18}}, Number{2000000000000000001, -18},
__LINE__},
{Number{-1414213562373095048, -18}, {Number{-1414213562373095048, -18},
Number{1414213562373095048, -18}, Number{1414213562373095048, -18},
Number{-1999999999999999997, -18}}, Number{-1999999999999999997, -18},
__LINE__},
{Number{-1414213562373095048, -18}, {Number{-1414213562373095048, -18},
Number{-1414213562373095049, -18}, Number{-1414213562373095049, -18},
Number{2, 0}}, Number{2, 0},
__LINE__},
{Number{3214285714285714278, -18}, {Number{3214285714285714278, -18},
Number{3111111111111111119, -18}, Number{3111111111111111119, -18},
Number{1000000000000000001, -17}}, Number{1000000000000000001, -17},
// Maximum mantissa range - rounds up to minMantissa*10 __LINE__},
// 1e19*1e19=1e38 // Maximum internal mantissa range - rounds up to
// minMantissa*10 1e19*1e19=1e38
{Number{
false, maxInternalMantissa, 0, Number::normalized{}},
Number{
false, maxInternalMantissa, 0, Number::normalized{}},
Number{1, 38},
__LINE__},
// Maximum mantissa range - same as int64
{Number{false, maxMantissa, 0, Number::normalized{}}, {Number{false, maxMantissa, 0, Number::normalized{}},
Number{false, maxMantissa, 0, Number::normalized{}}, Number{false, maxMantissa, 0, Number::normalized{}},
Number{1, 38}}, Number{85'070'591'730'234'615'85, 19},
__LINE__},
// Maximum int64 range // Maximum int64 range
// 85'070'591'730'234'615'847'396'907'784'232'501'249 // 85'070'591'730'234'615'847'396'907'784'232'501'249
{Number{Number::maxRep, 0}, {Number{Number::largestMantissa, 0},
Number{Number::maxRep, 0}, Number{Number::largestMantissa, 0},
Number{85'070'591'730'234'615'85, 19}}, Number{85'070'591'730'234'615'85, 19},
__LINE__},
}); });
tests(cSmall, cLarge); tests(cSmall, cLarge);
} }
@@ -971,6 +1100,12 @@ public:
}; };
*/ */
auto const maxInternalMantissa =
static_cast<std::uint64_t>(
static_cast<std::int64_t>(power(10, Number::mantissaLog()))) *
10 -
1;
auto const cSmall = std::to_array<Case>( auto const cSmall = std::to_array<Case>(
{{Number{2}, 2, Number{1414213562373095049, -18}}, {{Number{2}, 2, Number{1414213562373095049, -18}},
{Number{2'000'000}, 2, Number{1414213562373095049, -15}}, {Number{2'000'000}, 2, Number{1414213562373095049, -15}},
@@ -982,17 +1117,17 @@ public:
{Number{0}, 5, Number{0}}, {Number{0}, 5, Number{0}},
{Number{5625, -4}, 2, Number{75, -2}}}); {Number{5625, -4}, 2, Number{75, -2}}});
auto const cLarge = std::to_array<Case>({ auto const cLarge = std::to_array<Case>({
{Number{false, Number::maxMantissa() - 9, -1, Number::normalized{}}, {Number{false, maxInternalMantissa - 9, -1, Number::normalized{}},
2, 2,
Number{false, 999'999'999'999'999'999, -9, Number::normalized{}}}, Number{false, 999'999'999'999'999'999, -9, Number::normalized{}}},
{Number{false, Number::maxMantissa() - 9, 0, Number::normalized{}}, {Number{false, maxInternalMantissa - 9, 0, Number::normalized{}},
2, 2,
Number{ Number{
false, 3'162'277'660'168'379'330, -9, Number::normalized{}}}, false, 3'162'277'660'168'379'330, -9, Number::normalized{}}},
{Number{Number::maxRep}, {Number{Number::largestMantissa},
2, 2,
Number{false, 3'037'000'499'976049692, -9, Number::normalized{}}}, Number{false, 3'037'000'499'976049692, -9, Number::normalized{}}},
{Number{Number::maxRep}, {Number{Number::largestMantissa},
4, 4,
Number{false, 55'108'98747006743627, -14, Number::normalized{}}}, Number{false, 55'108'98747006743627, -14, Number::normalized{}}},
}); });
@@ -1042,6 +1177,9 @@ public:
} }
}; };
auto const maxInternalMantissa =
power(10, Number::mantissaLog()) * 10 - 1;
auto const cSmall = std::to_array<Number>({ auto const cSmall = std::to_array<Number>({
Number{2}, Number{2},
Number{2'000'000}, Number{2'000'000},
@@ -1051,7 +1189,10 @@ public:
Number{5, -1}, Number{5, -1},
Number{0}, Number{0},
Number{5625, -4}, Number{5625, -4},
Number{Number::maxRep}, Number{Number::largestMantissa},
maxInternalMantissa,
Number{Number::minMantissa(), 0, Number::unchecked{}},
Number{Number::maxMantissa(), 0, Number::unchecked{}},
}); });
test(cSmall); test(cSmall);
bool caught = false; bool caught = false;
@@ -1417,20 +1558,20 @@ public:
case MantissaRange::large: case MantissaRange::large:
// Test the edges // Test the edges
// ((exponent < -(28)) || (exponent > -(8))))) // ((exponent < -(28)) || (exponent > -(8)))))
test(Number::min(), "1e-32750"); test(Number::min(), "922337203685477581e-32768");
test(Number::max(), "9223372036854775807e32768"); test(Number::max(), "9223372036854775807e32768");
test(Number::lowest(), "-9223372036854775807e32768"); test(Number::lowest(), "-9223372036854775807e32768");
{ {
NumberRoundModeGuard mg(Number::towards_zero); NumberRoundModeGuard mg(Number::towards_zero);
auto const maxMantissa = Number::maxMantissa(); auto const maxMantissa = Number::maxMantissa();
BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999'999ULL); BEAST_EXPECT(maxMantissa == 9'223'372'036'854'775'807ULL);
test( test(
Number{false, maxMantissa, 0, Number::normalized{}}, Number{false, maxMantissa, 0, Number::normalized{}},
"9999999999999999990"); "9223372036854775807");
test( test(
Number{true, maxMantissa, 0, Number::normalized{}}, Number{true, maxMantissa, 0, Number::normalized{}},
"-9999999999999999990"); "-9223372036854775807");
test( test(
Number{std::numeric_limits<std::int64_t>::max(), 0}, Number{std::numeric_limits<std::int64_t>::max(), 0},
@@ -1671,7 +1812,7 @@ public:
Number const initalXrp{INITIAL_XRP}; Number const initalXrp{INITIAL_XRP};
BEAST_EXPECT(initalXrp.exponent() > 0); BEAST_EXPECT(initalXrp.exponent() > 0);
Number const maxInt64{Number::maxRep}; Number const maxInt64{Number::largestMantissa};
BEAST_EXPECT(maxInt64.exponent() > 0); BEAST_EXPECT(maxInt64.exponent() > 0);
// 85'070'591'730'234'615'865'843'651'857'942'052'864 - 38 digits // 85'070'591'730'234'615'865'843'651'857'942'052'864 - 38 digits
BEAST_EXPECT( BEAST_EXPECT(
@@ -1691,7 +1832,7 @@ public:
Number const initalXrp{INITIAL_XRP}; Number const initalXrp{INITIAL_XRP};
BEAST_EXPECT(initalXrp.exponent() <= 0); BEAST_EXPECT(initalXrp.exponent() <= 0);
Number const maxInt64{Number::maxRep}; Number const maxInt64{Number::largestMantissa};
BEAST_EXPECT(maxInt64.exponent() <= 0); BEAST_EXPECT(maxInt64.exponent() <= 0);
// 85'070'591'730'234'615'847'396'907'784'232'501'249 - 38 digits // 85'070'591'730'234'615'847'396'907'784'232'501'249 - 38 digits
BEAST_EXPECT( BEAST_EXPECT(
@@ -1699,16 +1840,247 @@ public:
NumberRoundModeGuard mg(Number::towards_zero); NumberRoundModeGuard mg(Number::towards_zero);
auto const maxMantissa = Number::maxMantissa(); {
Number const max = auto const maxInternalMantissa =
Number{false, maxMantissa, 0, Number::normalized{}}; static_cast<std::uint64_t>(static_cast<std::int64_t>(
BEAST_EXPECT(max.mantissa() == maxMantissa / 10); power(10, Number::mantissaLog()))) *
BEAST_EXPECT(max.exponent() == 1); 10 -
// 99'999'999'999'999'999'800'000'000'000'000'000'100 - also 38 1;
// digits
BEAST_EXPECT(( // Rounds down to fit under 2^63
power(max, 2) == Number const max =
Number{false, maxMantissa / 10 - 1, 20, Number::normalized{}})); Number{false, maxInternalMantissa, 0, Number::normalized{}};
// No alterations by the accessors
BEAST_EXPECT(max.mantissa() == maxInternalMantissa / 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,
maxInternalMantissa / 10 - 1,
20,
Number::normalized{}}));
}
{
auto const maxMantissa = Number::maxMantissa();
Number const max =
Number{false, maxMantissa, 0, Number::normalized{}};
// No alterations by the accessors
BEAST_EXPECT(max.mantissa() == maxMantissa);
BEAST_EXPECT(max.exponent() == 0);
// 85'070'591'730'234'615'847'396'907'784'232'501'249 - also 38
// digits
BEAST_EXPECT(
(power(max, 2) ==
Number{
false,
85'070'591'730'234'615'84,
19,
Number::normalized{}}));
}
}
}
void
testNormalizeToRange()
{
// Test edge-cases of normalizeToRange
auto const scale = Number::getMantissaScale();
testcase << "normalizeToRange " << to_string(scale);
auto test = [this](
Number const& n,
auto const rangeMin,
auto const rangeMax,
auto const expectedMantissa,
auto const expectedExponent,
auto const line) {
auto const normalized = n.normalizeToRange(rangeMin, rangeMax);
BEAST_EXPECTS(
normalized.first == expectedMantissa,
"Number " + to_string(n) + " scaled to " +
std::to_string(rangeMax) +
". Expected mantissa:" + std::to_string(expectedMantissa) +
", got: " + std::to_string(normalized.first) + " @ " +
std::to_string(line));
BEAST_EXPECTS(
normalized.second == expectedExponent,
"Number " + to_string(n) + " scaled to " +
std::to_string(rangeMax) +
". Expected exponent:" + std::to_string(expectedExponent) +
", got: " + std::to_string(normalized.second) + " @ " +
std::to_string(line));
};
std::int64_t constexpr iRangeMin = 100;
std::int64_t constexpr iRangeMax = 999;
std::uint64_t constexpr uRangeMin = 100;
std::uint64_t constexpr uRangeMax = 999;
constexpr static MantissaRange largeRange{MantissaRange::large};
std::int64_t constexpr iBigMin = largeRange.min;
std::int64_t constexpr iBigMax = largeRange.max;
auto const testSuite = [&](Number const& n,
auto const expectedSmallMantissa,
auto const expectedSmallExponent,
auto const expectedLargeMantissa,
auto const expectedLargeExponent,
auto const line) {
test(
n,
iRangeMin,
iRangeMax,
expectedSmallMantissa,
expectedSmallExponent,
line);
test(
n,
iBigMin,
iBigMax,
expectedLargeMantissa,
expectedLargeExponent,
line);
// Only test non-negative. testing a negative number with an
// unsigned range will assert, and asserts can't be tested.
if (n.signum() >= 0)
{
test(
n,
uRangeMin,
uRangeMax,
expectedSmallMantissa,
expectedSmallExponent,
line);
test(
n,
largeRange.min,
largeRange.max,
expectedLargeMantissa,
expectedLargeExponent,
line);
}
};
{
// zero
Number const n{0};
testSuite(
n,
0,
std::numeric_limits<int>::lowest(),
0,
std::numeric_limits<int>::lowest(),
__LINE__);
}
{
// Small positive number
Number const n{2};
testSuite(n, 200, -2, 2'000'000'000'000'000'000, -18, __LINE__);
}
{
// Negative number
Number const n{-2};
testSuite(n, -200, -2, -2'000'000'000'000'000'000, -18, __LINE__);
}
{
// Biggest valid mantissa
Number const n{Number::largestMantissa, 0, Number::normalized{}};
if (scale == MantissaRange::small)
// With the small mantissa range, the value rounds up. Because
// it rounds up, when scaling up to the full int64 range, it
// can't go over the max, so it is one digit smaller than the
// full value.
testSuite(n, 922, 16, 922'337'203'685'477'600, 1, __LINE__);
else
testSuite(n, 922, 16, Number::largestMantissa, 0, __LINE__);
}
{
// Biggest valid mantissa + 1
Number const n{
Number::largestMantissa + 1, 0, Number::normalized{}};
if (scale == MantissaRange::small)
// With the small mantissa range, the value rounds up. Because
// it rounds up, when scaling up to the full int64 range, it
// can't go over the max, so it is one digit smaller than the
// full value.
testSuite(n, 922, 16, 922'337'203'685'477'600, 1, __LINE__);
else
testSuite(n, 922, 16, 922'337'203'685'477'581, 1, __LINE__);
}
{
// Biggest valid mantissa + 2
Number const n{
Number::largestMantissa + 2, 0, Number::normalized{}};
if (scale == MantissaRange::small)
// With the small mantissa range, the value rounds up. Because
// it rounds up, when scaling up to the full int64 range, it
// can't go over the max, so it is one digit smaller than the
// full value.
testSuite(n, 922, 16, 922'337'203'685'477'600, 1, __LINE__);
else
testSuite(n, 922, 16, 922'337'203'685'477'581, 1, __LINE__);
}
{
// Biggest valid mantissa + 3
Number const n{
Number::largestMantissa + 3, 0, Number::normalized{}};
if (scale == MantissaRange::small)
// With the small mantissa range, the value rounds up. Because
// it rounds up, when scaling up to the full int64 range, it
// can't go over the max, so it is one digit smaller than the
// full value.
testSuite(n, 922, 16, 922'337'203'685'477'600, 1, __LINE__);
else
testSuite(n, 922, 16, 922'337'203'685'477'581, 1, __LINE__);
}
{
// int64 min
Number const n{std::numeric_limits<std::int64_t>::min(), 0};
if (scale == MantissaRange::small)
testSuite(n, -922, 16, -922'337'203'685'477'600, 1, __LINE__);
else
testSuite(n, -922, 16, -922'337'203'685'477'581, 1, __LINE__);
}
{
// int64 min + 1
Number const n{std::numeric_limits<std::int64_t>::min() + 1, 0};
if (scale == MantissaRange::small)
testSuite(n, -922, 16, -922'337'203'685'477'600, 1, __LINE__);
else
testSuite(n, -922, 16, -9'223'372'036'854'775'807, 0, __LINE__);
}
{
// int64 min - 1
// Need to cast to uint, even though we're dealing with a negative
// number to avoid overflow and UB
Number const n{
true,
-static_cast<std::uint64_t>(
std::numeric_limits<std::int64_t>::min()) +
1,
0,
Number::normalized{}};
if (scale == MantissaRange::small)
testSuite(n, -922, 16, -922'337'203'685'477'600, 1, __LINE__);
else
testSuite(n, -922, 16, -922'337'203'685'477'581, 1, __LINE__);
} }
} }
@@ -1739,6 +2111,7 @@ public:
test_truncate(); test_truncate();
testRounding(); testRounding();
testInt64(); testInt64();
testNormalizeToRange();
} }
} }
}; };

View File

@@ -176,8 +176,7 @@ getAssetsTotalScale(SLE::const_ref vaultSle)
{ {
if (!vaultSle) if (!vaultSle)
return Number::minExponent - 1; // LCOV_EXCL_LINE return Number::minExponent - 1; // LCOV_EXCL_LINE
return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)} return vaultSle->at(sfAssetsTotal).scale<STAmount>(vaultSle->at(sfAsset));
.exponent();
} }
TER TER

View File

@@ -19,10 +19,11 @@
#include <xrpl/protocol/SystemParameters.h> #include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h> #include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h> #include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/nftPageMask.h> #include <xrpl/protocol/nftPageMask.h>
#include <cstdint> #include <algorithm>
#include <cstddef>
#include <initializer_list>
#include <optional> #include <optional>
namespace xrpl { namespace xrpl {
@@ -2663,8 +2664,13 @@ ValidVault::visitEntry(
// Number balanceDelta will capture the difference (delta) between "before" // Number balanceDelta will capture the difference (delta) between "before"
// state (zero if created) and "after" state (zero if destroyed), so the // state (zero if created) and "after" state (zero if destroyed), so the
// invariants can validate that the change in account balances matches the // invariants can validate that the change in account balances matches the
// change in vault balances, stored to deltas_ at the end of this function. // balanceDelta captures the difference (delta) between "before"
Number balanceDelta{}; // state (zero if created) and "after" state (zero if destroyed), and
// preserves value scale (exponent) to round values to the same scale during
// validation. It is used to validate that the change in account
// balances matches the change in vault balances, stored to deltas_ at the
// end of this function.
DeltaInfo balanceDelta{numZero, std::nullopt};
std::int8_t sign = 0; std::int8_t sign = 0;
if (before) if (before)
@@ -2678,20 +2684,35 @@ ValidVault::visitEntry(
// At this moment we have no way of telling if this object holds // At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize. // vault shares or something else. Save it for finalize.
beforeMPTs_.push_back(Shares::make(*before)); beforeMPTs_.push_back(Shares::make(*before));
balanceDelta = static_cast<std::int64_t>( balanceDelta.delta = static_cast<std::int64_t>(
before->getFieldU64(sfOutstandingAmount)); before->getFieldU64(sfOutstandingAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = 1; sign = 1;
break; break;
case ltMPTOKEN: case ltMPTOKEN:
balanceDelta = balanceDelta.delta =
static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount)); static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = -1; sign = -1;
break; break;
case ltACCOUNT_ROOT: case ltACCOUNT_ROOT:
case ltRIPPLE_STATE: balanceDelta.delta = before->getFieldAmount(sfBalance);
balanceDelta = before->getFieldAmount(sfBalance); // Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
sign = -1; sign = -1;
break; break;
case ltRIPPLE_STATE: {
auto const amount = before->getFieldAmount(sfBalance);
balanceDelta.delta = amount;
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:; default:;
} }
} }
@@ -2707,20 +2728,36 @@ ValidVault::visitEntry(
// At this moment we have no way of telling if this object holds // At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize. // vault shares or something else. Save it for finalize.
afterMPTs_.push_back(Shares::make(*after)); afterMPTs_.push_back(Shares::make(*after));
balanceDelta -= Number(static_cast<std::int64_t>( balanceDelta.delta -= Number(static_cast<std::int64_t>(
after->getFieldU64(sfOutstandingAmount))); after->getFieldU64(sfOutstandingAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = 1; sign = 1;
break; break;
case ltMPTOKEN: case ltMPTOKEN:
balanceDelta -= Number( balanceDelta.delta -= Number(
static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount))); static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = -1; sign = -1;
break; break;
case ltACCOUNT_ROOT: case ltACCOUNT_ROOT:
case ltRIPPLE_STATE: balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
balanceDelta -= Number(after->getFieldAmount(sfBalance)); // Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
sign = -1; sign = -1;
break; break;
case ltRIPPLE_STATE: {
auto const amount = after->getFieldAmount(sfBalance);
balanceDelta.delta -= Number(amount);
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
if (amount.exponent() > balanceDelta.scale)
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:; default:;
} }
} }
@@ -2732,7 +2769,14 @@ ValidVault::visitEntry(
// transferred to the account. We intentionally do not compare balanceDelta // transferred to the account. We intentionally do not compare balanceDelta
// against zero, to avoid missing such updates. // against zero, to avoid missing such updates.
if (sign != 0) if (sign != 0)
deltas_[key] = balanceDelta * sign; {
XRPL_ASSERT_PARTS(
balanceDelta.scale,
"xrpl::ValidVault::visitEntry",
"scale initialized");
balanceDelta.delta *= sign;
deltas_[key] = balanceDelta;
}
} }
bool bool
@@ -3012,13 +3056,15 @@ ValidVault::finalize(
} }
auto const& vaultAsset = afterVault.asset; auto const& vaultAsset = afterVault.asset;
auto const deltaAssets = [&](AccountID const& id) -> std::optional<Number> { auto const deltaAssets =
[&](AccountID const& id) -> std::optional<DeltaInfo> {
auto const get = // auto const get = //
[&](auto const& it, std::int8_t sign = 1) -> std::optional<Number> { [&](auto const& it,
std::int8_t sign = 1) -> std::optional<DeltaInfo> {
if (it == deltas_.end()) if (it == deltas_.end())
return std::nullopt; return std::nullopt;
return it->second * sign; return DeltaInfo{it->second.delta * sign, it->second.scale};
}; };
return std::visit( return std::visit(
@@ -3039,7 +3085,7 @@ ValidVault::finalize(
}, },
vaultAsset.value()); vaultAsset.value());
}; };
auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> { auto const deltaAssetsTxAccount = [&]() -> std::optional<DeltaInfo> {
auto ret = deltaAssets(tx[sfAccount]); auto ret = deltaAssets(tx[sfAccount]);
// Nothing returned or not XRP transaction // Nothing returned or not XRP transaction
if (!ret.has_value() || !vaultAsset.native()) if (!ret.has_value() || !vaultAsset.native())
@@ -3050,13 +3096,14 @@ ValidVault::finalize(
delegate.has_value() && *delegate != tx[sfAccount]) delegate.has_value() && *delegate != tx[sfAccount])
return ret; return ret;
*ret += fee.drops(); ret->delta += fee.drops();
if (*ret == zero) if (ret->delta == zero)
return std::nullopt; return std::nullopt;
return ret; return ret;
}; };
auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> { auto const deltaShares =
[&](AccountID const& id) -> std::optional<DeltaInfo> {
auto const it = [&]() { auto const it = [&]() {
if (id == afterVault.pseudoId) if (id == afterVault.pseudoId)
return deltas_.find( return deltas_.find(
@@ -3064,7 +3111,7 @@ ValidVault::finalize(
return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key); return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
}(); }();
return it != deltas_.end() ? std::optional<Number>(it->second) return it != deltas_.end() ? std::optional<DeltaInfo>(it->second)
: std::nullopt; : std::nullopt;
}; };
@@ -3196,16 +3243,41 @@ ValidVault::finalize(
"xrpl::ValidVault::finalize : deposit updated a vault"); "xrpl::ValidVault::finalize : deposit updated a vault");
auto const& beforeVault = beforeVault_[0]; auto const& beforeVault = beforeVault_[0];
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); auto const maybeVaultDeltaAssets =
deltaAssets(afterVault.pseudoId);
if (!vaultDeltaAssets) if (!maybeVaultDeltaAssets)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault balance"; "Invariant failed: deposit must change vault balance";
return false; // That's all we can do return false; // That's all we can do
} }
if (*vaultDeltaAssets > tx[sfAmount]) // Get the coarsest scale to round calculations to
DeltaInfo totalDelta{
afterVault.assetsTotal - beforeVault.assetsTotal,
std::max(
afterVault.assetsTotal.scale<STAmount>(vaultAsset),
beforeVault.assetsTotal.scale<STAmount>(vaultAsset))};
DeltaInfo availableDelta{
afterVault.assetsAvailable - beforeVault.assetsAvailable,
std::max(
afterVault.assetsAvailable.scale<STAmount>(vaultAsset),
beforeVault.assetsAvailable.scale<STAmount>(
vaultAsset))};
auto const minScale = computeMinScale(
vaultAsset,
{
*maybeVaultDeltaAssets,
totalDelta,
availableDelta,
});
auto const vaultDeltaAssets = roundToAsset(
vaultAsset, maybeVaultDeltaAssets->delta, minScale);
auto const txAmount =
roundToAsset(vaultAsset, tx[sfAmount], minScale);
if (vaultDeltaAssets > txAmount)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: deposit must not change vault " "Invariant failed: deposit must not change vault "
@@ -3213,7 +3285,7 @@ ValidVault::finalize(
result = false; result = false;
} }
if (*vaultDeltaAssets <= zero) if (vaultDeltaAssets <= zero)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: deposit must increase vault balance"; "Invariant failed: deposit must increase vault balance";
@@ -3230,16 +3302,24 @@ ValidVault::finalize(
if (!issuerDeposit) if (!issuerDeposit)
{ {
auto const accountDeltaAssets = deltaAssetsTxAccount(); auto const maybeAccDeltaAssets = deltaAssetsTxAccount();
if (!accountDeltaAssets) if (!maybeAccDeltaAssets)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor " "Invariant failed: deposit must change depositor "
"balance"; "balance";
return false; return false;
} }
auto const localMinScale = std::max(
minScale,
computeMinScale(vaultAsset, {*maybeAccDeltaAssets}));
if (*accountDeltaAssets >= zero) auto const accountDeltaAssets = roundToAsset(
vaultAsset, maybeAccDeltaAssets->delta, localMinScale);
auto const localVaultDeltaAssets = roundToAsset(
vaultAsset, vaultDeltaAssets, localMinScale);
if (accountDeltaAssets >= zero)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: deposit must decrease depositor " "Invariant failed: deposit must decrease depositor "
@@ -3247,7 +3327,7 @@ ValidVault::finalize(
result = false; result = false;
} }
if (*accountDeltaAssets * -1 != *vaultDeltaAssets) if (localVaultDeltaAssets * -1 != accountDeltaAssets)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault and " "Invariant failed: deposit must change vault and "
@@ -3265,16 +3345,17 @@ ValidVault::finalize(
result = false; result = false;
} }
auto const accountDeltaShares = deltaShares(tx[sfAccount]); auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares) if (!maybeAccDeltaShares)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor " "Invariant failed: deposit must change depositor "
"shares"; "shares";
return false; // That's all we can do return false; // That's all we can do
} }
// We don't need to round shares, they are integral MPT
if (*accountDeltaShares <= zero) auto const& accountDeltaShares = *maybeAccDeltaShares;
if (accountDeltaShares.delta <= zero)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: deposit must increase depositor " "Invariant failed: deposit must increase depositor "
@@ -3282,15 +3363,19 @@ ValidVault::finalize(
result = false; result = false;
} }
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); auto const maybeVaultDeltaShares =
if (!vaultDeltaShares || *vaultDeltaShares == zero) deltaShares(afterVault.pseudoId);
if (!maybeVaultDeltaShares ||
maybeVaultDeltaShares->delta == zero)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault shares"; "Invariant failed: deposit must change vault shares";
return false; // That's all we can do return false; // That's all we can do
} }
if (*vaultDeltaShares * -1 != *accountDeltaShares) // We don't need to round shares, they are integral MPT
auto const& vaultDeltaShares = *maybeVaultDeltaShares;
if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor and " "Invariant failed: deposit must change depositor and "
@@ -3298,15 +3383,22 @@ ValidVault::finalize(
result = false; result = false;
} }
if (beforeVault.assetsTotal + *vaultDeltaAssets != auto const assetTotalDelta = roundToAsset(
afterVault.assetsTotal) vaultAsset,
afterVault.assetsTotal - beforeVault.assetsTotal,
minScale);
if (assetTotalDelta != vaultDeltaAssets)
{ {
JLOG(j.fatal()) << "Invariant failed: deposit and assets " JLOG(j.fatal()) << "Invariant failed: deposit and assets "
"outstanding must add up"; "outstanding must add up";
result = false; result = false;
} }
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
afterVault.assetsAvailable) auto const assetAvailableDelta = roundToAsset(
vaultAsset,
afterVault.assetsAvailable - beforeVault.assetsAvailable,
minScale);
if (assetAvailableDelta != vaultDeltaAssets)
{ {
JLOG(j.fatal()) << "Invariant failed: deposit and assets " JLOG(j.fatal()) << "Invariant failed: deposit and assets "
"available must add up"; "available must add up";
@@ -3324,22 +3416,41 @@ ValidVault::finalize(
"vault"); "vault");
auto const& beforeVault = beforeVault_[0]; auto const& beforeVault = beforeVault_[0];
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); auto const maybeVaultDeltaAssets =
deltaAssets(afterVault.pseudoId);
if (!vaultDeltaAssets) if (!maybeVaultDeltaAssets)
{ {
JLOG(j.fatal()) << "Invariant failed: withdrawal must " JLOG(j.fatal()) << "Invariant failed: withdrawal must "
"change vault balance"; "change vault balance";
return false; // That's all we can do return false; // That's all we can do
} }
if (*vaultDeltaAssets >= zero) // Get the most coarse scale to round calculations to
auto const totalDelta = DeltaInfo{
afterVault.assetsTotal - beforeVault.assetsTotal,
std::max(
afterVault.assetsTotal.scale<STAmount>(vaultAsset),
beforeVault.assetsTotal.scale<STAmount>(vaultAsset))};
auto const availableDelta = DeltaInfo{
afterVault.assetsAvailable - beforeVault.assetsAvailable,
std::max(
afterVault.assetsAvailable.scale<STAmount>(vaultAsset),
beforeVault.assetsAvailable.scale<STAmount>(
vaultAsset))};
auto const minScale = computeMinScale(
vaultAsset,
{*maybeVaultDeltaAssets, totalDelta, availableDelta});
auto const vaultPseudoDeltaAssets = roundToAsset(
vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultPseudoDeltaAssets >= zero)
{ {
JLOG(j.fatal()) << "Invariant failed: withdrawal must " JLOG(j.fatal()) << "Invariant failed: withdrawal must "
"decrease vault balance"; "decrease vault balance";
result = false; result = false;
} }
// Any payments (including withdrawal) going to the issuer // Any payments (including withdrawal) going to the issuer
// do not change their balance, but destroy funds instead. // do not change their balance, but destroy funds instead.
bool const issuerWithdrawal = [&]() -> bool { bool const issuerWithdrawal = [&]() -> bool {
@@ -3352,17 +3463,17 @@ ValidVault::finalize(
if (!issuerWithdrawal) if (!issuerWithdrawal)
{ {
auto const accountDeltaAssets = deltaAssetsTxAccount(); auto const maybeAccDelta = deltaAssetsTxAccount();
auto const otherAccountDelta = auto const maybeOtherAccDelta =
[&]() -> std::optional<Number> { [&]() -> std::optional<DeltaInfo> {
if (auto const destination = tx[~sfDestination]; if (auto const destination = tx[~sfDestination];
destination && *destination != tx[sfAccount]) destination && *destination != tx[sfAccount])
return deltaAssets(*destination); return deltaAssets(*destination);
return std::nullopt; return std::nullopt;
}(); }();
if (accountDeltaAssets.has_value() == if (maybeAccDelta.has_value() ==
otherAccountDelta.has_value()) maybeOtherAccDelta.has_value())
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change one " "Invariant failed: withdrawal must change one "
@@ -3371,10 +3482,18 @@ ValidVault::finalize(
} }
auto const destinationDelta = // auto const destinationDelta = //
accountDeltaAssets ? *accountDeltaAssets maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta;
: *otherAccountDelta;
if (destinationDelta <= zero) // the scale of destinationDelta can be coarser than
// minScale, so we take that into account when rounding
auto const localMinScale = std::max(
minScale,
computeMinScale(vaultAsset, {destinationDelta}));
auto const roundedDestinationDelta = roundToAsset(
vaultAsset, destinationDelta.delta, localMinScale);
if (roundedDestinationDelta <= zero)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: withdrawal must increase " "Invariant failed: withdrawal must increase "
@@ -3382,7 +3501,9 @@ ValidVault::finalize(
result = false; result = false;
} }
if (*vaultDeltaAssets * -1 != destinationDelta) auto const localPseudoDeltaAssets = roundToAsset(
vaultAsset, vaultPseudoDeltaAssets, localMinScale);
if (localPseudoDeltaAssets * -1 != roundedDestinationDelta)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault " "Invariant failed: withdrawal must change vault "
@@ -3390,7 +3511,7 @@ ValidVault::finalize(
result = false; result = false;
} }
} }
// We don't need to round shares, they are integral MPT
auto const accountDeltaShares = deltaShares(tx[sfAccount]); auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares) if (!accountDeltaShares)
{ {
@@ -3400,23 +3521,23 @@ ValidVault::finalize(
return false; return false;
} }
if (*accountDeltaShares >= zero) if (accountDeltaShares->delta >= zero)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: withdrawal must decrease depositor " "Invariant failed: withdrawal must decrease depositor "
"shares"; "shares";
result = false; result = false;
} }
// We don't need to round shares, they are integral MPT
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero) if (!vaultDeltaShares || vaultDeltaShares->delta == zero)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault shares"; "Invariant failed: withdrawal must change vault shares";
return false; // That's all we can do return false; // That's all we can do
} }
if (*vaultDeltaShares * -1 != *accountDeltaShares) if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change depositor " "Invariant failed: withdrawal must change depositor "
@@ -3424,17 +3545,24 @@ ValidVault::finalize(
result = false; result = false;
} }
auto const assetTotalDelta = roundToAsset(
vaultAsset,
afterVault.assetsTotal - beforeVault.assetsTotal,
minScale);
// Note, vaultBalance is negative (see check above) // Note, vaultBalance is negative (see check above)
if (beforeVault.assetsTotal + *vaultDeltaAssets != if (assetTotalDelta != vaultPseudoDeltaAssets)
afterVault.assetsTotal)
{ {
JLOG(j.fatal()) << "Invariant failed: withdrawal and " JLOG(j.fatal()) << "Invariant failed: withdrawal and "
"assets outstanding must add up"; "assets outstanding must add up";
result = false; result = false;
} }
if (beforeVault.assetsAvailable + *vaultDeltaAssets != auto const assetAvailableDelta = roundToAsset(
afterVault.assetsAvailable) vaultAsset,
afterVault.assetsAvailable - beforeVault.assetsAvailable,
minScale);
if (assetAvailableDelta != vaultPseudoDeltaAssets)
{ {
JLOG(j.fatal()) << "Invariant failed: withdrawal and " JLOG(j.fatal()) << "Invariant failed: withdrawal and "
"assets available must add up"; "assets available must add up";
@@ -3468,10 +3596,30 @@ ValidVault::finalize(
} }
} }
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); auto const maybeVaultDeltaAssets =
if (vaultDeltaAssets) deltaAssets(afterVault.pseudoId);
if (maybeVaultDeltaAssets)
{ {
if (*vaultDeltaAssets >= zero) auto const totalDelta = DeltaInfo{
afterVault.assetsTotal - beforeVault.assetsTotal,
std::max(
afterVault.assetsTotal.scale<STAmount>(vaultAsset),
beforeVault.assetsTotal.scale<STAmount>(
vaultAsset))};
auto const availableDelta = DeltaInfo{
afterVault.assetsAvailable -
beforeVault.assetsAvailable,
std::max(
afterVault.assetsAvailable.scale<STAmount>(
vaultAsset),
beforeVault.assetsAvailable.scale<STAmount>(
vaultAsset))};
auto const minScale = computeMinScale(
vaultAsset,
{*maybeVaultDeltaAssets, totalDelta, availableDelta});
auto const vaultDeltaAssets = roundToAsset(
vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultDeltaAssets >= zero)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease vault " "Invariant failed: clawback must decrease vault "
@@ -3479,8 +3627,11 @@ ValidVault::finalize(
result = false; result = false;
} }
if (beforeVault.assetsTotal + *vaultDeltaAssets != auto const assetsTotalDelta = roundToAsset(
afterVault.assetsTotal) vaultAsset,
afterVault.assetsTotal - beforeVault.assetsTotal,
minScale);
if (assetsTotalDelta != vaultDeltaAssets)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: clawback and assets outstanding " "Invariant failed: clawback and assets outstanding "
@@ -3488,8 +3639,12 @@ ValidVault::finalize(
result = false; result = false;
} }
if (beforeVault.assetsAvailable + *vaultDeltaAssets != auto const assetAvailableDelta = roundToAsset(
afterVault.assetsAvailable) vaultAsset,
afterVault.assetsAvailable -
beforeVault.assetsAvailable,
minScale);
if (assetAvailableDelta != vaultDeltaAssets)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: clawback and assets available " "Invariant failed: clawback and assets available "
@@ -3504,15 +3659,15 @@ ValidVault::finalize(
return false; // That's all we can do return false; // That's all we can do
} }
auto const accountDeltaShares = deltaShares(tx[sfHolder]); // We don't need to round shares, they are integral MPT
if (!accountDeltaShares) auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]);
if (!maybeAccountDeltaShares)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder shares"; "Invariant failed: clawback must change holder shares";
return false; // That's all we can do return false; // That's all we can do
} }
if (maybeAccountDeltaShares->delta >= zero)
if (*accountDeltaShares >= zero)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease holder " "Invariant failed: clawback must decrease holder "
@@ -3520,15 +3675,17 @@ ValidVault::finalize(
result = false; result = false;
} }
// We don't need to round shares, they are integral MPT
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero) if (!vaultDeltaShares || vaultDeltaShares->delta == zero)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: clawback must change vault shares"; "Invariant failed: clawback must change vault shares";
return false; // That's all we can do return false; // That's all we can do
} }
if (*vaultDeltaShares * -1 != *accountDeltaShares) if (vaultDeltaShares->delta * -1 !=
maybeAccountDeltaShares->delta)
{ {
JLOG(j.fatal()) << // JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder and " "Invariant failed: clawback must change holder and "
@@ -3566,4 +3723,23 @@ ValidVault::finalize(
return true; return true;
} }
[[nodiscard]] std::int32_t
ValidVault::computeMinScale(
Asset const& asset,
std::initializer_list<DeltaInfo const> numbers)
{
if (numbers.size() == 0)
return 0;
auto const max = std::max_element(
numbers.begin(),
numbers.end(),
[](auto const& a, auto const& b) -> bool { return a.scale < b.scale; });
XRPL_ASSERT_PARTS(
max->scale,
"xrpl::ValidVault::computeMinScale",
"scale set for destinationDelta");
return max->scale.value_or(STAmount::cMaxOffset);
}
} // namespace xrpl } // namespace xrpl

View File

@@ -9,7 +9,6 @@
#include <xrpl/protocol/STTx.h> #include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h> #include <xrpl/protocol/TER.h>
#include <cstdint>
#include <tuple> #include <tuple>
#include <unordered_set> #include <unordered_set>
@@ -880,11 +879,19 @@ class ValidVault
Shares static make(SLE const&); Shares static make(SLE const&);
}; };
public:
struct DeltaInfo final
{
Number delta = numZero;
std::optional<int> scale;
};
private:
std::vector<Vault> afterVault_ = {}; std::vector<Vault> afterVault_ = {};
std::vector<Shares> afterMPTs_ = {}; std::vector<Shares> afterMPTs_ = {};
std::vector<Vault> beforeVault_ = {}; std::vector<Vault> beforeVault_ = {};
std::vector<Shares> beforeMPTs_ = {}; std::vector<Shares> beforeMPTs_ = {};
std::unordered_map<uint256, Number> deltas_ = {}; std::unordered_map<uint256, DeltaInfo> deltas_ = {};
public: public:
void void
@@ -900,6 +907,12 @@ public:
XRPAmount const, XRPAmount const,
ReadView const&, ReadView const&,
beast::Journal const&); beast::Journal const&);
// Compute the coarsest scale required to represent all numbers
[[nodiscard]] static std::int32_t
computeMinScale(
Asset const& asset,
std::initializer_list<DeltaInfo const> numbers);
}; };
// additional invariant checks can be declared above and then added to this // additional invariant checks can be declared above and then added to this

View File

@@ -125,7 +125,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
tenthBipsOfValue( tenthBipsOfValue(
currentDebtTotal, currentDebtTotal,
TenthBips32(sleBroker->at(sfCoverRateMinimum))), TenthBips32(sleBroker->at(sfCoverRateMinimum))),
currentDebtTotal.exponent()); currentDebtTotal.scale<STAmount>(vaultAsset));
}(); }();
if (coverAvail < amount) if (coverAvail < amount)
return tecINSUFFICIENT_FUNDS; return tecINSUFFICIENT_FUNDS;