mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-06 10:16:45 +00:00
fix: Fix a rounding error at the Number::maxRep cusp (#7051)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
This commit is contained in:
@@ -6,7 +6,10 @@
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <boost/multiprecision/number.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
@@ -19,6 +22,24 @@ namespace xrpl {
|
||||
|
||||
class Number_test : public beast::unit_test::Suite
|
||||
{
|
||||
using BigInt = boost::multiprecision::cpp_int;
|
||||
|
||||
static std::string
|
||||
fmt(BigInt const& value)
|
||||
{
|
||||
auto s = to_string(value);
|
||||
std::string out;
|
||||
int count = 0;
|
||||
for (auto it = s.rbegin(); it != s.rend(); ++it)
|
||||
{
|
||||
if (count != 0 && count % 3 == 0 && (isdigit(*it) != 0))
|
||||
out.insert(out.begin(), '_');
|
||||
out.insert(out.begin(), *it);
|
||||
++count;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testZero()
|
||||
@@ -178,7 +199,6 @@ public:
|
||||
{Number{true, 9'999'999'999'999'999'999ULL, -37, Number::Normalized{}},
|
||||
Number{1'000'000'000'000'000'000, -18},
|
||||
Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}}},
|
||||
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}},
|
||||
{Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}},
|
||||
// Test extremes
|
||||
{
|
||||
@@ -189,16 +209,22 @@ public:
|
||||
Number{2, 19},
|
||||
},
|
||||
{
|
||||
// Does not round. Mantissas are going to be > maxRep, so if
|
||||
// Does not round. Mantissas are going to be > kMaxRep, so if
|
||||
// added together as uint64_t's, the result will overflow.
|
||||
// With addition using uint128_t, there's no problem. After
|
||||
// normalizing, the resulting mantissa ends up less than
|
||||
// maxRep.
|
||||
// kMaxRep.
|
||||
Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}},
|
||||
Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}},
|
||||
Number{false, 1'999'999'999'999'999'998ULL, 1, Number::Normalized{}},
|
||||
},
|
||||
});
|
||||
auto const cLargeLegacy = std::to_array<Case>({
|
||||
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}},
|
||||
});
|
||||
auto const cLargeCorrected = std::to_array<Case>({
|
||||
{Number{Number::kMaxRep}, Number{6, -1}, Number{(Number::kMaxRep / 10) + 1, 1}},
|
||||
});
|
||||
auto test = [this](auto const& c) {
|
||||
for (auto const& [x, y, z] : c)
|
||||
{
|
||||
@@ -215,6 +241,14 @@ public:
|
||||
else
|
||||
{
|
||||
test(cLarge);
|
||||
if (scale == MantissaRange::MantissaScale::LargeLegacy)
|
||||
{
|
||||
test(cLargeLegacy);
|
||||
}
|
||||
else
|
||||
{
|
||||
test(cLargeCorrected);
|
||||
}
|
||||
}
|
||||
{
|
||||
bool caught = false;
|
||||
@@ -835,7 +869,7 @@ public:
|
||||
/*
|
||||
auto tests = [&](auto const& cSmall, auto const& cLarge) {
|
||||
test(cSmall);
|
||||
if (scale != MantissaRange::mantissa_scale::small)
|
||||
if (scale != MantissaRange::MantissaScale::Small)
|
||||
test(cLarge);
|
||||
};
|
||||
*/
|
||||
@@ -1266,6 +1300,7 @@ public:
|
||||
"9223372036854775e3");
|
||||
}
|
||||
break;
|
||||
case MantissaRange::MantissaScale::LargeLegacy:
|
||||
case MantissaRange::MantissaScale::Large:
|
||||
// Test the edges
|
||||
// ((exponent < -(28)) || (exponent > -(8)))))
|
||||
@@ -1551,11 +1586,48 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testUpwardRoundsDown()
|
||||
{
|
||||
testcase << "upward rounding produces a value below exact at kMaxRep cusp";
|
||||
|
||||
NumberMantissaScaleGuard const mg{MantissaRange::MantissaScale::Large};
|
||||
NumberRoundModeGuard const rg{Number::RoundingMode::Upward};
|
||||
|
||||
constexpr std::int64_t kAValue = 1'000'000'000'000'049'863LL;
|
||||
constexpr std::int64_t kBValue = 9'223'372'036'854'315'903LL;
|
||||
|
||||
Number const a = kAValue;
|
||||
Number const b = kBValue;
|
||||
Number const product = a * b;
|
||||
|
||||
// Exact reference in BigInt.
|
||||
BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue);
|
||||
|
||||
// What Number actually stored.
|
||||
BigInt storedValue = BigInt(product.mantissa());
|
||||
for (int i = 0; i < product.exponent(); ++i)
|
||||
storedValue *= 10;
|
||||
|
||||
BigInt const signedDifference = storedValue - exactProduct;
|
||||
|
||||
log << "\n"
|
||||
<< " a = " << fmt(BigInt(kAValue)) << "\n"
|
||||
<< " b = " << fmt(BigInt(kBValue)) << "\n"
|
||||
<< " exact a*b = " << fmt(exactProduct) << "\n"
|
||||
<< " stored = " << fmt(storedValue) << "\n"
|
||||
<< " stored - exact = " << fmt(signedDifference) << "\n"
|
||||
<< " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n";
|
||||
|
||||
BEAST_EXPECT(signedDifference >= 0);
|
||||
BEAST_EXPECT(product.mantissa() == (std::numeric_limits<std::int64_t>::max() / 10) + 1);
|
||||
BEAST_EXPECT(product.exponent() == 19);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
for (auto const scale :
|
||||
{MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::Large})
|
||||
for (auto const scale : MantissaRange::getAllScales())
|
||||
{
|
||||
NumberMantissaScaleGuard const sg(scale);
|
||||
testZero();
|
||||
@@ -1580,6 +1652,8 @@ public:
|
||||
testRounding();
|
||||
testInt64();
|
||||
}
|
||||
// This test sets its own number range
|
||||
testUpwardRoundsDown();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user