Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3cb447a
Add optional enforcement of valid integer range to Number
ximinez Nov 5, 2025
24f37d7
Make all STNumber fields "soeDEFAULT"
ximinez Nov 5, 2025
b605a2c
Add integer enforcement when converting to XRP/MPTAmount to Number
ximinez Nov 5, 2025
cb6df19
Fix build error - avoid copy
ximinez Nov 5, 2025
0175dd7
Catch up the consequences of Number changes
ximinez Nov 5, 2025
8e56af2
Add a distinction between a "valid" and a "representable" Number
ximinez Nov 7, 2025
fabc7bd
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 8, 2025
edb9b16
fix: Use ".value()" instead of "->" when with STObject::Proxy objects
ximinez Nov 7, 2025
1648ead
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 10, 2025
a45b43e
Update include/xrpl/basics/Number.h
ximinez Nov 10, 2025
ff9270b
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 11, 2025
a3db23e
Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-number
ximinez Nov 12, 2025
5845c5c
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 13, 2025
2854e6b
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 15, 2025
8822b53
Rip out about half the code: levels, enforcement, and STAmount changes
ximinez Nov 17, 2025
596365d
fixup! Rip out about half the code: levels, enforcement, and STAmount…
ximinez Nov 17, 2025
7974545
Fixes build errors and test failures
ximinez Nov 17, 2025
bba4b44
fix: Number: Do not attempt to take the negative of an unsigned
ximinez Nov 17, 2025
2338c55
test fix: Remove cases with Vault scale 18
ximinez Nov 17, 2025
67796cf
Change the vaultMaximumIOUScale from 13 to 15
ximinez Nov 18, 2025
2e02c33
Expand the description of Number::isInteger_
ximinez Nov 18, 2025
6503e6d
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 19, 2025
1e47c76
Rewrite to clarify Number limitations, enforce limits in conversion
ximinez Nov 19, 2025
82553c2
Avoid some null dereferences in tests
ximinez Nov 19, 2025
dc283ce
Add a couple of high value MPT tests
ximinez Nov 19, 2025
687b9cc
Add an RAII NumberIntegerOverflowGuard class, and update tests
ximinez Nov 19, 2025
9fae186
Fix large integer overflow
ximinez Nov 19, 2025
bd9346a
Fix Vault tests due to Number changes
ximinez Nov 19, 2025
3b421cd
More test and logging updates
ximinez Nov 19, 2025
f9293e6
Set the global rules for every transaction step
ximinez Nov 20, 2025
3868158
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 21, 2025
e130119
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 25, 2025
9ab1734
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 25, 2025
7b31969
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 26, 2025
2e9bdb9
Merge branch 'develop' into ximinez/lending-number
ximinez Nov 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 186 additions & 7 deletions include/xrpl/basics/Number.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,58 @@ class Number;
std::string
to_string(Number const& amount);

template <typename T>
constexpr bool
isPowerOfTen(T value)
{
while (value >= 10 && value % 10 == 0)
value /= 10;
return value == 1;
}

class Number
{
using rep = std::int64_t;
rep mantissa_{0};
int exponent_{std::numeric_limits<int>::lowest()};

// Within Number itself limited_ is informational only. It is not
// serialized, transmitted, or used in calculations in any way. It is used
// only to indicate that a given Number's absolute value _might_ need to be
// less than maxIntValue or maxMantissa.
//
// It is a one-way switch. Once it's on, it stays on. It is also
// transmissible in that any operation (e.g +, /, power, etc.) involving a
// Number with this flag will have a result with this flag.
//
// The flag is checked in the following places:
// 1. "fits()" indicates whether the Number fits into the safe range of
// -maxIntValue to maxIntValue.
// 2. "representable()" indicates whether the Number can accurately
// represent an integer, meaning that it fits withing the allowable range
// of -maxMantissa to maxMantissa. Values larger than this will be
// truncated before the decimal point, rendering the value inaccurate.
// 3. In "operator rep()", which explicitly converts the number into a
// 64-bit integer, if the integer value grows larger than maxMantissa
// while it's being computed, AND one of the SingleAssetVault (or
// LendingProtocol, coming soon) amendments are enabled, the operator
// will throw a "std::overflow_error" as if the number had overflowed the
// limits of the 64-bit integer range.
//
// The Number is usually only going to be checked in transactions, based on
// the specific transaction logic, and is entirely context dependent.
//
bool limited_ = false;

public:
// The range for the mantissa when normalized
constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL;
constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL;
constexpr static rep minMantissa = 1'000'000'000'000'000LL;
static_assert(isPowerOfTen(minMantissa));
constexpr static rep maxMantissa = minMantissa * 10 - 1;
static_assert(maxMantissa == 9'999'999'999'999'999LL);

constexpr static rep maxIntValue = maxMantissa / 100;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how was this number chosen? could we have chosen a larger value?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how was this number chosen? could we have chosen a larger value?

There's a discussion in the comments in https://ripplelabs.atlassian.net/browse/RIPD-4045.

Since that's not public, the tl;dr is that I started with maxMantissa / 10, which would give the STNumber fields in Vault room to grow if they get, for example, extra interest from a special case payment in a Loan. I was concerned that wouldn't be enough overhead. The answer was

Let’s try to go with a safer, conservative limit as in one that reduces the truncation as much as possible

This is intended to be a temporary solution to get the feature available without a major rewrite. A fuller solution should be available in a version or two which allows the entire range of valid numbers.

static_assert(maxIntValue == 99'999'999'999'999LL);

// The range for the exponent when normalized
constexpr static int minExponent = -32768;
Expand All @@ -35,15 +77,53 @@ class Number

explicit constexpr Number() = default;

Number(rep mantissa);
explicit Number(rep mantissa, int exponent);
Number(rep mantissa, bool limited = false);
explicit Number(rep mantissa, int exponent, bool limited = false);
explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept;
constexpr Number(Number const& other) = default;
constexpr Number(Number&& other) = default;

~Number() = default;

constexpr Number&
operator=(Number const& other);
constexpr Number&
operator=(Number&& other);

constexpr rep
mantissa() const noexcept;
constexpr int
exponent() const noexcept;

// Sets the limited_ flag. See the description of limited for how it works.
// Note that the flag can only change from false to true, not from true to
// false.
void
setLimited(bool limited);

// Gets the current value of the limited_ flag. See the description of
// limited for how it works.
bool
getLimited() const noexcept;

// 1. "fits()" indicates whether the Number fits into the safe range of
// -maxIntValue to maxIntValue.
bool
fits() const noexcept;
bool
// 2. "representable()" indicates whether the Number can accurately
// represent an integer, meaning that it fits withing the allowable range
// of -maxMantissa to maxMantissa. Values larger than this will be
// truncated before the decimal point, rendering the value inaccurate.
representable() const noexcept;
/// Combines setLimited(bool) and fits()
bool
fits(bool limited);
/// Because this function is const, it should only be used for one-off
/// checks
bool
fits(bool limited) const;

constexpr Number
operator+() const noexcept;
constexpr Number
Expand Down Expand Up @@ -78,6 +158,13 @@ class Number
* are explicit. This design encourages and facilitates the use of Number
* as the preferred type for floating point arithmetic as it makes
* "mixed mode" more convenient, e.g. MPTAmount + Number.

3. In "operator rep()", which explicitly converts the number into a
64-bit integer, if the integer value grows larger than maxMantissa
while it's being computed, AND one of the SingleAssetVault (or
LendingProtocol, coming soon) amendments are enabled, the operator
will throw a "std::overflow_error" as if the number had overflowed
the limits of the 64-bit integer range.
*/
explicit
operator rep() const; // round to nearest, even on tie
Expand Down Expand Up @@ -181,9 +268,31 @@ class Number
static rounding_mode
setround(rounding_mode mode);

// Thread local integer overflow control. See overflowLargeIntegers_ for
// more info.
static bool
getEnforceIntegerOverflow();
// Thread local integer overflow control. See overflowLargeIntegers_ for
// more info.
static void
setEnforceIntegerOverflow(bool enforce);

private:
static thread_local rounding_mode mode_;

// This flag defaults to false. It is set and cleared by
// "setCurrentTransactionRules" in Rules.cpp. It will be set to true if and
// only if any of the SingleAssetVault or LendingProtocol amendments are
// enabled.
//
// If set, then any explicit conversions from Number to rep (which is
// std::int64_t) of Numbers that are not representable (which means their
// magnitude is larger than maxMantissa, and thus they will lose integer
// precision) will throw a std::overflow_error. Note that this coversion
// will already throw an overflow error if the Number is larger 2^63.
// See also "operator rep()".
static thread_local bool overflowLargeIntegers_;

void
normalize();
constexpr bool
Expand All @@ -197,16 +306,46 @@ inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept
{
}

inline Number::Number(rep mantissa, int exponent)
: mantissa_{mantissa}, exponent_{exponent}
inline Number::Number(rep mantissa, int exponent, bool limited)
: mantissa_{mantissa}, exponent_{exponent}, limited_(limited)
{
normalize();
}

inline Number::Number(rep mantissa) : Number{mantissa, 0}
inline Number::Number(rep mantissa, bool limited) : Number{mantissa, 0, limited}
{
}

constexpr Number&
Number::operator=(Number const& other)
{
if (this != &other)
{
mantissa_ = other.mantissa_;
exponent_ = other.exponent_;
if (!limited_)
limited_ = other.limited_;
}

return *this;
}

constexpr Number&
Number::operator=(Number&& other)
{
if (this != &other)
{
// std::move doesn't really do anything for these types, but
// this is future-proof in case the types ever change
mantissa_ = std::move(other.mantissa_);
exponent_ = std::move(other.exponent_);
if (!limited_)
limited_ = std::move(other.limited_);
}

return *this;
}

inline constexpr Number::rep
Number::mantissa() const noexcept
{
Expand All @@ -219,6 +358,20 @@ Number::exponent() const noexcept
return exponent_;
}

inline void
Number::setLimited(bool limited)
{
if (limited_)
return;
limited_ = limited;
}

inline bool
Number::getLimited() const noexcept
{
return limited_;
}

inline constexpr Number
Number::operator+() const noexcept
{
Expand Down Expand Up @@ -404,6 +557,32 @@ class NumberRoundModeGuard
operator=(NumberRoundModeGuard const&) = delete;
};

// Sets the EnforceIntegerOverflow flag and restores the old value when it
// leaves scope. Since Number doesn't have that facility, we'll build it here.
//
// This class may only end up needed in tests
class NumberIntegerOverflowGuard
{
bool const saved_;

public:
explicit NumberIntegerOverflowGuard(bool enforce) noexcept
: saved_{Number::getEnforceIntegerOverflow()}
{
Number::setEnforceIntegerOverflow(enforce);
}

~NumberIntegerOverflowGuard()
{
Number::setEnforceIntegerOverflow(saved_);
}

NumberIntegerOverflowGuard(NumberIntegerOverflowGuard const&) = delete;

NumberIntegerOverflowGuard&
operator=(NumberIntegerOverflowGuard const&) = delete;
};

} // namespace ripple

#endif // XRPL_BASICS_NUMBER_H_INCLUDED
22 changes: 21 additions & 1 deletion include/xrpl/protocol/Asset.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,27 @@ class Asset
bool
native() const
{
return holds<Issue>() && get<Issue>().native();
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
return issue.native();
if constexpr (std::is_same_v<TIss, MPTIssue>)
return false;
},
issue_);
}

bool
integral() const
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
return issue.native();
if constexpr (std::is_same_v<TIss, MPTIssue>)
return true;
},
issue_);
}

friend constexpr bool
Expand Down
2 changes: 1 addition & 1 deletion include/xrpl/protocol/MPTAmount.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class MPTAmount : private boost::totally_ordered<MPTAmount>,

operator Number() const noexcept
{
return value();
return {value(), true};
}

/** Return the sign of the amount */
Expand Down
6 changes: 4 additions & 2 deletions include/xrpl/protocol/Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,11 @@ std::uint8_t constexpr vaultStrategyFirstComeFirstServe = 1;
/** Default IOU scale factor for a Vault */
std::uint8_t constexpr vaultDefaultIOUScale = 6;
/** Maximum scale factor for a Vault. The number is chosen to ensure that
1 IOU can be always converted to shares.
1 IOU can be always converted to shares and will fit into Number.
10^16 > Number::maxMantissa
In the future, this should be increased to 18.
10^19 > maxMPTokenAmount (2^64-1) > 10^18 */
std::uint8_t constexpr vaultMaximumIOUScale = 18;
std::uint8_t constexpr vaultMaximumIOUScale = 15;

/** Maximum recursion depth for vault shares being put as an asset inside
* another vault; counted from 0 */
Expand Down
29 changes: 28 additions & 1 deletion include/xrpl/protocol/STAmount.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,24 @@ class STAmount final : public STBase, public CountedObject<STAmount>
STAmount(MPTAmount const& amount, MPTIssue const& mptIssue);
operator Number() const;

// Determines if the "Number" representation of the amount can fit within
// the Number's soft range limits. Converts the amount to a Number, using
// the automatic conversion rules defined in "operator Number()", and in
// "XRPAmount::operator Number()", "MPTAmount::operator Number()", and
// "IOUAmount::operator Number()", then returns the result of
// "Number::fits()". See "Number::fits()" for more information.
bool
numberFits() const noexcept;
// Determines if the "Number" representation of the amount can fit within
// the Number's hard range limits. Converts the amount to a Number, using
// the automatic conversion rules defined in "operator Number()", and in
// "XRPAmount::operator Number()", "MPTAmount::operator Number()", and
// "IOUAmount::operator Number()", then returns the result of
// "Number::representable()". See "Number::representable()" for more
// information.
bool
representableNumber() const noexcept;

//--------------------------------------------------------------------------
//
// Observers
Expand All @@ -155,6 +173,9 @@ class STAmount final : public STBase, public CountedObject<STAmount>
int
exponent() const noexcept;

bool
integral() const noexcept;

bool
native() const noexcept;

Expand Down Expand Up @@ -435,6 +456,12 @@ STAmount::exponent() const noexcept
return mOffset;
}

inline bool
STAmount::integral() const noexcept
{
return mAsset.integral();
}

inline bool
STAmount::native() const noexcept
{
Expand Down Expand Up @@ -553,7 +580,7 @@ STAmount::clear()
{
// The -100 is used to allow 0 to sort less than a small positive values
// which have a negative exponent.
mOffset = native() ? 0 : -100;
mOffset = integral() ? 0 : -100;
mValue = 0;
mIsNegative = false;
}
Expand Down
4 changes: 4 additions & 0 deletions include/xrpl/protocol/STObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,8 @@ class STObject::Proxy
value_type
operator*() const;

/// Do not use operator->() unless the field is required, or you've checked
/// that it's set.
T const*
operator->() const;

Expand Down Expand Up @@ -718,6 +720,8 @@ STObject::Proxy<T>::operator*() const -> value_type
return this->value();
}

/// Do not use operator->() unless the field is required, or you've checked that
/// it's set.
template <class T>
T const*
STObject::Proxy<T>::operator->() const
Expand Down
Loading