Merge branch 'develop' into ximinez/online-delete-gaps

This commit is contained in:
Ed Hennis
2026-05-27 12:06:52 -04:00
committed by GitHub
15 changed files with 646 additions and 361 deletions

View File

@@ -10,9 +10,11 @@
#include <iterator>
#include <limits>
#include <numeric>
#include <set>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <utility>
#ifdef _MSC_VER
@@ -28,7 +30,76 @@ using int128_t = __int128_t;
namespace xrpl {
thread_local Number::RoundingMode Number::mode = Number::RoundingMode::ToNearest;
thread_local std::reference_wrapper<MantissaRange const> Number::kRange = kLargeRange;
thread_local std::reference_wrapper<MantissaRange const> Number::kRange =
MantissaRange::getMantissaRange(MantissaRange::MantissaScale::Large);
std::set<MantissaRange::MantissaScale> const&
MantissaRange::getAllScales()
{
static std::set<MantissaRange::MantissaScale> const kScales = {
MantissaRange::MantissaScale::Small,
MantissaRange::MantissaScale::LargeLegacy,
MantissaRange::MantissaScale::Large,
};
return kScales;
}
std::unordered_map<MantissaRange::MantissaScale, MantissaRange> const&
MantissaRange::getRanges()
{
static auto const kMap = []() {
std::unordered_map<MantissaScale, MantissaRange> map;
for (auto const scale : getAllScales())
{
map.emplace(scale, scale);
}
// Use these constexpr declarations to do static_asserts to verify the MantissaRanges are
// created correctly, but nothing else.
{
[[maybe_unused]]
constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Small};
static_assert(isPowerOfTen(kRange.min));
static_assert(kRange.min == 1'000'000'000'000'000LL);
static_assert(kRange.max == 9'999'999'999'999'999LL);
static_assert(kRange.log == 15);
static_assert(kRange.min < Number::kMaxRep);
static_assert(kRange.max < Number::kMaxRep);
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled);
}
{
[[maybe_unused]]
constexpr static MantissaRange kRange{MantissaRange::MantissaScale::LargeLegacy};
static_assert(isPowerOfTen(kRange.min));
static_assert(kRange.min == 1'000'000'000'000'000'000ULL);
static_assert(kRange.max == rep(9'999'999'999'999'999'999ULL));
static_assert(kRange.log == 18);
static_assert(kRange.min < Number::kMaxRep);
static_assert(kRange.max > Number::kMaxRep);
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled);
}
{
[[maybe_unused]]
constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Large};
static_assert(isPowerOfTen(kRange.min));
static_assert(kRange.min == 1'000'000'000'000'000'000ULL);
static_assert(kRange.max == rep(9'999'999'999'999'999'999ULL));
static_assert(kRange.log == 18);
static_assert(kRange.min < Number::kMaxRep);
static_assert(kRange.max > Number::kMaxRep);
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Enabled);
}
return map;
}();
return kMap;
}
MantissaRange const&
MantissaRange::getMantissaRange(MantissaScale scale)
{
return getRanges().at(scale);
}
Number::RoundingMode
Number::getround()
@@ -51,10 +122,37 @@ Number::getMantissaScale()
void
Number::setMantissaScale(MantissaRange::MantissaScale scale)
{
if (scale != MantissaRange::MantissaScale::Small &&
scale != MantissaRange::MantissaScale::Large)
if (!MantissaRange::getAllScales().contains(scale))
logicError("Unknown mantissa scale");
kRange = scale == MantissaRange::MantissaScale::Small ? kSmallRange : kLargeRange;
kRange = MantissaRange::getMantissaRange(scale);
}
// Optimization equivalent to:
// auto r = static_cast<unsigned>(u % 10);
// u /= 10;
// return r;
// Derived from Hacker's Delight Second Edition Chapter 10
// by Henry S. Warren, Jr.
static inline unsigned
divu10(uint128_t& u)
{
// q = u * 0.75
auto q = (u >> 1) + (u >> 2);
// iterate towards q = u * 0.8
q += q >> 4;
q += q >> 8;
q += q >> 16;
q += q >> 32;
q += q >> 64;
// q /= 8 approximately == u / 10
q >>= 3;
// r = u - q * 10 approximately == u % 10
auto r = static_cast<unsigned>(u - ((q << 3) + (q << 1)));
// correction c is 1 if r >= 10 else 0
auto c = (r + 6) >> 4;
u = q + c;
r -= c * 10;
return r;
}
// Guard
@@ -92,6 +190,18 @@ public:
unsigned
pop() noexcept;
/** Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in
* this Guard.
*
* Substitute for:
push(mantissa % 10);
mantissa /= 10;
++exponent;
*/
template <class T>
void
doDropDigit(T& mantissa, int& exponent) noexcept;
// Indicate round direction: 1 is up, -1 is down, 0 is even
// This enables the client to round towards nearest, and on
// tie, round towards even.
@@ -107,6 +217,7 @@ public:
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
std::string location);
// Modify the result to the correctly rounded value
@@ -168,6 +279,27 @@ Number::Guard::pop() noexcept
return d;
}
template <class T>
void
Number::Guard::doDropDigit(T& mantissa, int& exponent) noexcept
{
push(mantissa % 10);
mantissa /= 10;
++exponent;
}
// Use the divu10 optimization for uint128s
template <>
void
Number::Guard::doDropDigit<uint128_t>(uint128_t& mantissa, int& exponent) noexcept
{
// The following is optimization for:
// push(static_cast<unsigned>(mantissa % 10));
// mantissa /= 10;
push(divu10(mantissa));
++exponent;
}
// Returns:
// -1 if Guard is less than half
// 0 if Guard is exactly half
@@ -242,18 +374,60 @@ Number::Guard::doRoundUp(
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
std::string location)
{
auto r = round();
if (r == 1 || (r == 0 && (mantissa & 1) == 1))
{
++mantissa;
// Ensure mantissa after incrementing fits within both the
// min/maxMantissa range and is a valid "rep".
if (mantissa > maxMantissa || mantissa > kMaxRep)
auto const safeToIncrement = [&maxMantissa](auto const& mantissa) {
return mantissa < maxMantissa && mantissa < kMaxRep;
};
if (cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled)
{
mantissa /= 10;
++exponent;
// Ensure mantissa after incrementing fits within both the
// min/maxMantissa range and is a valid "rep".
if (safeToIncrement(mantissa))
{
// Nothing unusual here, just increment the mantissa
++mantissa;
}
else
{
// Incrementing the mantissa will require dividing, which will require rounding. So
// _don't_ increment the mantissa. Instead, divide and round recursively. It should
// be impossible to recurse more than once, because once the mantissa is divided by
// 10, it will be _well_ under maxMantissa and kMaxRep, so adding 1 will have no
// change of bringing it back over.
doDropDigit(mantissa, exponent);
XRPL_ASSERT_PARTS(
safeToIncrement(mantissa),
"xrpl::Number::Guard::doRoundUp",
"can't recurse more than once");
doRoundUp(
negative,
mantissa,
exponent,
minMantissa,
maxMantissa,
cuspRoundingFixEnabled,
location);
return;
}
}
else
{
// Need to preserve the incorrect behavior until the fix amendment can be retired,
// because otherwise would risk an unplanned ledger fork.
++mantissa;
// Ensure mantissa after incrementing fits within both the
// min/maxMantissa range and is a valid "rep".
if (mantissa > maxMantissa || mantissa > kMaxRep)
{
// Don't use doDropDigit here
mantissa /= 10;
++exponent;
}
}
}
bringIntoRange(negative, mantissa, exponent, minMantissa);
@@ -293,9 +467,9 @@ Number::Guard::doRound(rep& drops, std::string location) const
{
static_assert(sizeof(internalrep) == sizeof(rep));
// This should be impossible, because it's impossible to represent
// "maxRep + 0.6" in Number, regardless of the scale. There aren't
// enough digits available. You'd either get a mantissa of "maxRep"
// or "(maxRep + 1) / 10", neither of which will round up when
// "kMaxRep + 0.6" in Number, regardless of the scale. There aren't
// enough digits available. You'd either get a mantissa of "kMaxRep"
// or "(kMaxRep + 1) / 10", neither of which will round up when
// converting to rep, though the latter might overflow _before_
// rounding.
Throw<std::overflow_error>(std::string(location)); // LCOV_EXCL_LINE
@@ -331,29 +505,11 @@ Number::externalToInternal(rep mantissa)
return static_cast<internalrep>(-temp);
}
constexpr Number
Number::oneSmall()
{
return Number{false, Number::kSmallRange.min, -Number::kSmallRange.log, Number::Unchecked{}};
};
constexpr Number kOneSml = Number::oneSmall();
constexpr Number
Number::oneLarge()
{
return Number{false, Number::kLargeRange.min, -Number::kLargeRange.log, Number::Unchecked{}};
};
constexpr Number kOneLrg = Number::oneLarge();
Number
Number::one()
{
if (&kRange.get() == &kSmallRange)
return kOneSml;
XRPL_ASSERT(&kRange.get() == &kLargeRange, "Number::one() : valid range");
return kOneLrg;
auto const& range = kRange.get();
return Number{false, range.min, -range.log, Number::Unchecked{}};
}
// Use the member names in this static function for now so the diff is cleaner
@@ -365,7 +521,8 @@ doNormalize(
T& mantissa,
int& exponent,
MantissaRange::rep const& minMantissa,
MantissaRange::rep const& maxMantissa)
MantissaRange::rep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
{
static constexpr auto kMinExponent = Number::kMinExponent;
static constexpr auto kMaxExponent = Number::kMaxExponent;
@@ -394,9 +551,7 @@ doNormalize(
{
if (exponent >= kMaxExponent)
throw std::overflow_error("Number::normalize 1");
g.push(m % 10);
m /= 10;
++exponent;
g.doDropDigit(m, exponent);
}
if ((exponent < kMinExponent) || (m < minMantissa))
{
@@ -407,7 +562,7 @@ doNormalize(
}
// 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
// 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.
//
@@ -415,26 +570,31 @@ doNormalize(
// 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
// 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.
// mantissa() will return mantissa / 10, and exponent() will return
// exponent + 1.
if (m > kMaxRep)
{
if (exponent >= kMaxExponent)
throw std::overflow_error("Number::normalize 1.5");
g.push(m % 10);
m /= 10;
++exponent;
g.doDropDigit(m, 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)
// modification, it must be less than kMaxRep. In other words, the original
// value should have been no more than kMaxRep * 10.
// (kMaxRep * 10 > maxMantissa)
XRPL_ASSERT_PARTS(m <= kMaxRep, "xrpl::doNormalize", "intermediate mantissa fits in int64");
mantissa = m;
g.doRoundUp(negative, mantissa, exponent, minMantissa, maxMantissa, "Number::normalize 2");
g.doRoundUp(
negative,
mantissa,
exponent,
minMantissa,
maxMantissa,
cuspRoundingFixEnabled,
"Number::normalize 2");
XRPL_ASSERT_PARTS(
mantissa >= minMantissa && mantissa <= maxMantissa,
"xrpl::doNormalize",
@@ -448,9 +608,10 @@ Number::normalize<uint128_t>(
uint128_t& mantissa,
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa)
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
{
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa);
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled);
}
template <>
@@ -460,9 +621,10 @@ Number::normalize<unsigned long long>(
unsigned long long& mantissa,
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa)
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
{
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa);
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled);
}
template <>
@@ -472,16 +634,16 @@ Number::normalize<unsigned long>(
unsigned long& mantissa,
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa)
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
{
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa);
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled);
}
void
Number::normalize()
Number::normalize(MantissaRange const& range)
{
auto const& range = kRange.get();
normalize(negative_, mantissa_, exponent_, range.min, range.max);
normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFixEnabled);
}
// Copy the number, but set a new exponent. Because the mantissa doesn't change,
@@ -542,9 +704,7 @@ Number::operator+=(Number const& y)
g.setNegative();
do
{
g.push(xm % 10);
xm /= 10;
++xe;
g.doDropDigit(xm, xe);
} while (xe < ye);
}
else if (xe > ye)
@@ -553,26 +713,30 @@ Number::operator+=(Number const& y)
g.setNegative();
do
{
g.push(ym % 10);
ym /= 10;
++ye;
g.doDropDigit(ym, ye);
} while (xe > ye);
}
auto const& range = kRange.get();
auto const& minMantissa = range.min;
auto const& maxMantissa = range.max;
auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled;
if (xn == yn)
{
xm += ym;
if (xm > maxMantissa || xm > kMaxRep)
{
g.push(xm % 10);
xm /= 10;
++xe;
g.doDropDigit(xm, xe);
}
g.doRoundUp(xn, xm, xe, minMantissa, maxMantissa, "Number::addition overflow");
g.doRoundUp(
xn,
xm,
xe,
minMantissa,
maxMantissa,
cuspRoundingFixEnabled,
"Number::addition overflow");
}
else
{
@@ -598,38 +762,10 @@ Number::operator+=(Number const& y)
negative_ = xn;
mantissa_ = static_cast<internalrep>(xm);
exponent_ = xe;
normalize();
normalize(range);
return *this;
}
// Optimization equivalent to:
// auto r = static_cast<unsigned>(u % 10);
// u /= 10;
// return r;
// Derived from Hacker's Delight Second Edition Chapter 10
// by Henry S. Warren, Jr.
static inline unsigned
divu10(uint128_t& u)
{
// q = u * 0.75
auto q = (u >> 1) + (u >> 2);
// iterate towards q = u * 0.8
q += q >> 4;
q += q >> 8;
q += q >> 16;
q += q >> 32;
q += q >> 64;
// q /= 8 approximately == u / 10
q >>= 3;
// r = u - q * 10 approximately == u % 10
auto r = static_cast<unsigned>(u - ((q << 3) + (q << 1)));
// correction c is 1 if r >= 10 else 0
auto c = (r + 6) >> 4;
u = q + c;
r -= c * 10;
return r;
}
Number&
Number::operator*=(Number const& y)
{
@@ -667,15 +803,13 @@ Number::operator*=(Number const& y)
auto const& range = kRange.get();
auto const& minMantissa = range.min;
auto const& maxMantissa = range.max;
auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled;
while (zm > maxMantissa || zm > kMaxRep)
{
// The following is optimization for:
// g.push(static_cast<unsigned>(zm % 10));
// zm /= 10;
g.push(divu10(zm));
++ze;
g.doDropDigit(zm, ze);
}
xm = static_cast<internalrep>(zm);
xe = ze;
g.doRoundUp(
@@ -684,12 +818,13 @@ Number::operator*=(Number const& y)
xe,
minMantissa,
maxMantissa,
cuspRoundingFixEnabled,
"Number::multiplication overflow : exponent is " + std::to_string(xe));
negative_ = zn;
mantissa_ = xm;
exponent_ = xe;
normalize();
normalize(range);
return *this;
}
@@ -721,6 +856,7 @@ Number::operator/=(Number const& y)
auto const& range = kRange.get();
auto const& minMantissa = range.min;
auto const& maxMantissa = range.max;
auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled;
// Shift by 10^17 gives greatest precision while not overflowing
// uint128_t or the cast back to int64_t
@@ -728,8 +864,6 @@ Number::operator/=(Number const& y)
// log(2^128,10) ~ 38.5
// largeRange.log = 18, fits in 10^19
// f can be up to 10^(38-19) = 10^19 safely
static_assert(kSmallRange.log == 15);
static_assert(kLargeRange.log == 18);
bool const small = Number::getMantissaScale() == MantissaRange::MantissaScale::Small;
uint128_t const f = small ? 100'000'000'000'000'000 : 10'000'000'000'000'000'000ULL;
XRPL_ASSERT_PARTS(f >= minMantissa * 10, "Number::operator/=", "factor expected size");
@@ -779,7 +913,7 @@ Number::operator/=(Number const& y)
ze -= 3;
}
}
normalize(zn, zm, ze, minMantissa, maxMantissa);
normalize(zn, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled);
negative_ = zn;
mantissa_ = static_cast<internalrep>(zm);
exponent_ = ze;
@@ -801,10 +935,9 @@ operator rep() const
g.setNegative();
drops = -drops;
}
for (; offset < 0; ++offset)
while (offset < 0)
{
g.push(drops % 10);
drops /= 10;
g.doDropDigit(drops, offset);
}
for (; offset > 0; --offset)
{
@@ -831,7 +964,7 @@ Number::truncate() const noexcept
}
// We are guaranteed that normalize() will never throw an exception
// because exponent is either negative or zero at this point.
ret.normalize();
ret.normalize(kRange);
return ret;
}

View File

@@ -56,7 +56,7 @@ IOUAmount::fromNumber(Number const& number)
// to normalize, which calls fromNumber
IOUAmount result{};
std::tie(result.mantissa_, result.exponent_) =
number.normalizeToRange(kMinMantissa, kMaxMantissa);
number.normalizeToRange<kMinMantissa, kMaxMantissa>();
return result;
}

View File

@@ -7,6 +7,7 @@
#include <xrpl/beast/hash/uhash.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/STVector256.h>
#include <memory>
@@ -38,15 +39,68 @@ setCurrentTransactionRules(std::optional<Rules> r)
// Make global changes associated with the rules before the value is moved.
// Push the appropriate setting, instead of having the class pull every time
// the value is needed. That could get expensive fast.
bool const enableLargeNumbers =
// If any new conditions with new amendments are added, those amendments must also be added to
// useRulesGuards.
bool const enableVaultNumbers =
!r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol));
Number::setMantissaScale(
enableLargeNumbers ? MantissaRange::MantissaScale::Large
: MantissaRange::MantissaScale::Small);
bool const enableCuspRoundingFix = !r || r->enabled(fixCleanup3_2_0);
XRPL_ASSERT(
!r || useRulesGuards(*r) == (enableCuspRoundingFix || enableVaultNumbers),
"setCurrentTransactionRules : rule decisions match");
// Declare the range this way to keep clang-tidy from complaining
auto const range = [enableCuspRoundingFix, enableVaultNumbers]() {
if (enableVaultNumbers)
{
if (enableCuspRoundingFix)
{
return MantissaRange::MantissaScale::Large;
}
return MantissaRange::MantissaScale::LargeLegacy;
}
return MantissaRange::MantissaScale::Small;
}();
Number::setMantissaScale(range);
*getCurrentTransactionRulesRef() = std::move(r);
}
bool
useRulesGuards(Rules const& rules)
{
// The list of amendments used here - to decide whether to create a RulesGuard - must be a
// superset of the list used to figure out which mantissa scale to use in
// setCurrentTransactionRules. Additional amendments can be added if desired.
//
// As soon as any one of these amendments is retired, this whole function can be removed, along
// with createGuards, and any other callers, and the first set of guards can be created directly
// at the call site, without using optional.
return rules.enabled(fixCleanup3_2_0) || rules.enabled(featureSingleAssetVault) ||
rules.enabled(featureLendingProtocol);
}
void
createGuards(
Rules const& rules,
std::optional<NumberSO>& stNumberSO,
std::optional<CurrentTransactionRulesGuard>& rulesGuard,
std::optional<NumberMantissaScaleGuard>& mantissaScaleGuard)
{
if (useRulesGuards(rules))
{
// raii classes for the current ledger rules.
// fixUniversalNumber predates the rulesGuard and should be replaced.
stNumberSO.emplace(rules.enabled(fixUniversalNumber));
rulesGuard.emplace(rules);
}
else
{
// Without those features enabled, always use the old number rules.
mantissaScaleGuard.emplace(MantissaRange::MantissaScale::Small);
}
}
class Rules::Impl
{
private:

View File

@@ -96,7 +96,8 @@ STNumber::add(Serializer& s) const
// Json. Regardless, the only time we should be serializing an
// STNumber is when the scale is large.
XRPL_ASSERT_PARTS(
Number::getMantissaScale() == MantissaRange::MantissaScale::Large,
Number::getMantissaScale() == MantissaRange::MantissaScale::LargeLegacy ||
Number::getMantissaScale() == MantissaRange::MantissaScale::Large,
"xrpl::STNumber::add",
"STNumber only used with large mantissa scale");
#endif

View File

@@ -7,7 +7,6 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
@@ -66,26 +65,15 @@ withTxnType(Rules const& rules, TxType txnType, F&& f)
// so these need to be more global.
//
// To prevent unintentional side effects on existing checks, they will be
// set for every operation only once SingleAssetVault (or later
// LendingProtocol) are enabled.
// set for every operation only once at least one of the relevant amendments
// are enabled.
//
// See also Transactor::operator().
//
std::optional<NumberSO> stNumberSO;
std::optional<CurrentTransactionRulesGuard> rulesGuard;
std::optional<NumberMantissaScaleGuard> mantissaScaleGuard;
if (rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol))
{
// raii classes for the current ledger rules.
// fixUniversalNumber predates the rulesGuard and should be replaced.
stNumberSO.emplace(rules.enabled(fixUniversalNumber));
rulesGuard.emplace(rules);
}
else
{
// Without those features enabled, always use the old number rules.
mantissaScaleGuard.emplace(MantissaRange::MantissaScale::Small);
}
createGuards(rules, stNumberSO, rulesGuard, mantissaScaleGuard);
switch (txnType)
{

View File

@@ -65,10 +65,15 @@ namespace xrpl::test {
/**
* Tests of AMM that use offers too.
*/
struct AMMExtended_test : public jtx::AMMTest
class AMMExtended_test : public jtx::AMMTest
{
// Use small Number mantissas for the life of this test.
NumberMantissaScaleGuard const sg{xrpl::MantissaRange::MantissaScale::Small};
NumberMantissaScaleGuard const sg_{xrpl::MantissaRange::MantissaScale::Small};
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
FeatureBitset const all_{
testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
private:
void
@@ -1349,37 +1354,33 @@ private:
testOffers()
{
using namespace jtx;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
FeatureBitset const all{
testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
testRmFundedOffer(all);
testRmFundedOffer(all - fixAMMv1_1 - fixAMMv1_3);
testEnforceNoRipple(all);
testFillModes(all);
testOfferCrossWithXRP(all);
testOfferCrossWithLimitOverride(all);
testCurrencyConversionEntire(all);
testCurrencyConversionInParts(all);
testCrossCurrencyStartXRP(all);
testCrossCurrencyEndXRP(all);
testCrossCurrencyBridged(all);
testOfferFeesConsumeFunds(all);
testOfferCreateThenCross(all);
testSellFlagExceedLimit(all);
testGatewayCrossCurrency(all);
testGatewayCrossCurrency(all - fixAMMv1_1 - fixAMMv1_3);
testBridgedCross(all);
testSellWithFillOrKill(all);
testTransferRateOffer(all);
testSelfIssueOffer(all);
testBadPathAssert(all);
testSellFlagBasic(all);
testDirectToDirectPath(all);
testDirectToDirectPath(all - fixAMMv1_1 - fixAMMv1_3);
testRequireAuth(all);
testMissingAuth(all);
testRmFundedOffer(all_);
testRmFundedOffer(all_ - fixAMMv1_1 - fixAMMv1_3);
testEnforceNoRipple(all_);
testFillModes(all_);
testOfferCrossWithXRP(all_);
testOfferCrossWithLimitOverride(all_);
testCurrencyConversionEntire(all_);
testCurrencyConversionInParts(all_);
testCrossCurrencyStartXRP(all_);
testCrossCurrencyEndXRP(all_);
testCrossCurrencyBridged(all_);
testOfferFeesConsumeFunds(all_);
testOfferCreateThenCross(all_);
testSellFlagExceedLimit(all_);
testGatewayCrossCurrency(all_);
testGatewayCrossCurrency(all_ - fixAMMv1_1 - fixAMMv1_3);
testBridgedCross(all_);
testSellWithFillOrKill(all_);
testTransferRateOffer(all_);
testSelfIssueOffer(all_);
testBadPathAssert(all_);
testSellFlagBasic(all_);
testDirectToDirectPath(all_);
testDirectToDirectPath(all_ - fixAMMv1_1 - fixAMMv1_3);
testRequireAuth(all_);
testMissingAuth(all_);
}
void
@@ -3516,15 +3517,11 @@ private:
testFlow()
{
using namespace jtx;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas in the transaction engine
FeatureBitset const all{
testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
testFalseDry(all);
testBookStep(all);
testTransferRateNoOwnerFee(all);
testTransferRateNoOwnerFee(all - fixAMMv1_1 - fixAMMv1_3);
testFalseDry(all_);
testBookStep(all_);
testTransferRateNoOwnerFee(all_);
testTransferRateNoOwnerFee(all_ - fixAMMv1_1 - fixAMMv1_3);
testLimitQuality();
testXRPPathLoop();
}
@@ -3533,34 +3530,22 @@ private:
testCrossingLimits()
{
using namespace jtx;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas in the transaction engine
FeatureBitset const all{
testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
testStepLimit(all);
testStepLimit(all - fixAMMv1_1 - fixAMMv1_3);
testStepLimit(all_);
testStepLimit(all_ - fixAMMv1_1 - fixAMMv1_3);
}
void
testDeliverMin()
{
using namespace jtx;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas in the transaction engine
FeatureBitset const all{
testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
testConvertAllOfAnAsset(all);
testConvertAllOfAnAsset(all - fixAMMv1_1 - fixAMMv1_3);
testConvertAllOfAnAsset(all_);
testConvertAllOfAnAsset(all_ - fixAMMv1_1 - fixAMMv1_3);
}
void
testDepositAuth()
{
// For now, just disable SAV entirely, which locks in the small Number
// mantissas in the transaction engine
FeatureBitset const all{
jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
testPayment(all);
testPayment(all_);
testPayIOU();
}
@@ -3568,13 +3553,9 @@ private:
testFreeze()
{
using namespace test::jtx;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas in the transaction engine
FeatureBitset const sa{
testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
testRippleState(sa);
testGlobalFreeze(sa);
testOffersWhenFrozen(sa);
testRippleState(all_);
testGlobalFreeze(all_);
testOffersWhenFrozen(all_);
}
void

View File

@@ -2625,10 +2625,6 @@ private:
using namespace jtx;
using namespace std::chrono;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
features = features - featureSingleAssetVault - featureLendingProtocol;
// Auction slot initially is owned by AMM creator, who pays 0 price.
// Bid 110 tokens. Pay bidMin.
@@ -3337,11 +3333,6 @@ private:
testcase("Basic Payment");
using namespace jtx;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
features =
features - featureSingleAssetVault - featureLendingProtocol - featureLendingProtocol;
// Payment 100USD for 100XRP.
// Force one path with tfNoRippleDirect.
testAMM(

View File

@@ -4767,109 +4767,119 @@ class Invariants_test : public beast::unit_test::Suite
std::vector<ValidVault::DeltaInfo> values;
};
NumberMantissaScaleGuard const g{MantissaRange::MantissaScale::Large};
auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
return {.delta = n, .scale = scale(n, 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)
for (auto const mantissaScale : {
MantissaRange::MantissaScale::LargeLegacy,
MantissaRange::MantissaScale::Large,
})
{
testcase("vault computeCoarsestScale: " + tc.name);
NumberMantissaScaleGuard const g{mantissaScale};
auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
return {.delta = n, .scale = scale(n, vaultAsset.raw())};
};
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 computeCoarsestScale: " + tc.name);
auto const actualScale = ValidVault::computeCoarsestScale(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)
auto const testCases = std::vector<TestCase>{
{
first = num.delta;
firstRounded = roundToAsset(vaultAsset, num.delta, actualScale);
continue;
}
auto const numRounded = roundToAsset(vaultAsset, num.delta, actualScale);
.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 computeCoarsestScale: " + tc.name);
auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
BEAST_EXPECTS(
numRounded != firstRounded,
"at a scale of " + std::to_string(actualScale) + " " + to_string(num.delta) +
" == " + to_string(*first));
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 computeCoarsestScale: " + tc.name);
auto const actualScale = ValidVault::computeCoarsestScale(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));
}
}
}
}

View File

@@ -156,8 +156,7 @@ public:
BEAST_EXPECTS(result == expected, ss.str());
};
for (auto const mantissaSize :
{MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::Large})
for (auto const mantissaSize : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const mg(mantissaSize);

View File

@@ -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();
}
};

View File

@@ -280,8 +280,7 @@ struct STNumber_test : public beast::unit_test::Suite
{
static_assert(!std::is_convertible_v<STNumber*, Number*>);
for (auto const scale :
{MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::Large})
for (auto const scale : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const sg(scale);
testcase << to_string(Number::getMantissaScale());

View File

@@ -177,8 +177,7 @@ public:
auto const all = testableAmendments();
for (auto const& feats : {all - featureSingleAssetVault - featureLendingProtocol, all})
{
for (auto const mantissaSize :
{MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::Large})
for (auto const mantissaSize : MantissaRange::getAllScales())
{
// Regardless of the features enabled, RPC is controlled by
// the global mantissa size. And since it's a thread-local,