Use Number for IOUAmount and STAmount arithmetic

* Guarded by amendment fixUniversalNumber
* Produces slightly better accuracy in some computations.
This commit is contained in:
Howard Hinnant
2022-06-01 15:51:00 -04:00
committed by Elliot Lee
parent 48e804c40c
commit a82ad5ba76
14 changed files with 233 additions and 222 deletions

View File

@@ -293,6 +293,7 @@ TxQ::MaybeTx::apply(Application& app, OpenView& view, beast::Journal j)
// If the rules or flags change, preflight again // If the rules or flags change, preflight again
assert(pfresult); assert(pfresult);
STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)};
NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)};
if (pfresult->rules != view.rules() || pfresult->flags != flags) if (pfresult->rules != view.rules() || pfresult->flags != flags)
{ {
@@ -717,6 +718,7 @@ TxQ::apply(
beast::Journal j) beast::Journal j)
{ {
STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)};
NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)};
// See if the transaction paid a high enough fee that it can go straight // See if the transaction paid a high enough fee that it can go straight
// into the ledger. // into the ledger.

View File

@@ -36,7 +36,7 @@ struct AmountSpec
union union
{ {
XRPAmount xrp; XRPAmount xrp;
IOUAmount iou; IOUAmount iou = {};
}; };
std::optional<AccountID> issuer; std::optional<AccountID> issuer;
std::optional<Currency> currency; std::optional<Currency> currency;
@@ -64,7 +64,7 @@ struct EitherAmount
union union
{ {
IOUAmount iou; IOUAmount iou = {};
XRPAmount xrp; XRPAmount xrp;
}; };

View File

@@ -782,6 +782,7 @@ Transactor::operator()()
JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID(); JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID();
STAmountSO stAmountSO{view().rules().enabled(fixSTAmountCanonicalize)}; STAmountSO stAmountSO{view().rules().enabled(fixSTAmountCanonicalize)};
NumberSO stNumberSO{view().rules().enabled(fixUniversalNumber)};
#ifdef DEBUG #ifdef DEBUG
{ {

View File

@@ -114,6 +114,7 @@ apply(
beast::Journal j) beast::Journal j)
{ {
STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)};
NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)};
auto pfresult = preflight(app, view.rules(), tx, flags, j); auto pfresult = preflight(app, view.rules(), tx, flags, j);
auto pcresult = preclaim(pfresult, app, view); auto pcresult = preclaim(pfresult, app, view);

View File

@@ -20,6 +20,8 @@
#ifndef RIPPLE_BASICS_IOUAMOUNT_H_INCLUDED #ifndef RIPPLE_BASICS_IOUAMOUNT_H_INCLUDED
#define RIPPLE_BASICS_IOUAMOUNT_H_INCLUDED #define RIPPLE_BASICS_IOUAMOUNT_H_INCLUDED
#include <ripple/basics/LocalValue.h>
#include <ripple/basics/Number.h>
#include <ripple/beast/utility/Zero.h> #include <ripple/beast/utility/Zero.h>
#include <boost/operators.hpp> #include <boost/operators.hpp>
#include <cstdint> #include <cstdint>
@@ -56,84 +58,119 @@ private:
public: public:
IOUAmount() = default; IOUAmount() = default;
IOUAmount(IOUAmount const& other) = default; explicit IOUAmount(Number const& other);
IOUAmount& IOUAmount(beast::Zero);
operator=(IOUAmount const& other) = default; IOUAmount(std::int64_t mantissa, int exponent);
IOUAmount(beast::Zero) IOUAmount& operator=(beast::Zero);
{
*this = beast::zero;
}
IOUAmount(std::int64_t mantissa, int exponent) operator Number() const;
: mantissa_(mantissa), exponent_(exponent)
{
normalize();
}
IOUAmount& operator=(beast::Zero)
{
// The -100 is used to allow 0 to sort less than small positive values
// which will have a large negative exponent.
mantissa_ = 0;
exponent_ = -100;
return *this;
}
IOUAmount& IOUAmount&
operator+=(IOUAmount const& other); operator+=(IOUAmount const& other);
IOUAmount& IOUAmount&
operator-=(IOUAmount const& other) operator-=(IOUAmount const& other);
{
*this += -other;
return *this;
}
IOUAmount IOUAmount
operator-() const operator-() const;
{
return {-mantissa_, exponent_};
}
bool bool
operator==(IOUAmount const& other) const operator==(IOUAmount const& other) const;
{
return exponent_ == other.exponent_ && mantissa_ == other.mantissa_;
}
bool bool
operator<(IOUAmount const& other) const; operator<(IOUAmount const& other) const;
/** Returns true if the amount is not zero */ /** Returns true if the amount is not zero */
explicit operator bool() const noexcept explicit operator bool() const noexcept;
{
return mantissa_ != 0;
}
/** Return the sign of the amount */ /** Return the sign of the amount */
int int
signum() const noexcept signum() const noexcept;
{
return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0);
}
int int
exponent() const noexcept exponent() const noexcept;
{
return exponent_;
}
std::int64_t std::int64_t
mantissa() const noexcept mantissa() const noexcept;
{
return mantissa_;
}
static IOUAmount static IOUAmount
minPositiveAmount(); minPositiveAmount();
}; };
inline IOUAmount::IOUAmount(beast::Zero)
{
*this = beast::zero;
}
inline IOUAmount::IOUAmount(std::int64_t mantissa, int exponent)
: mantissa_(mantissa), exponent_(exponent)
{
normalize();
}
inline IOUAmount& IOUAmount::operator=(beast::Zero)
{
// The -100 is used to allow 0 to sort less than small positive values
// which will have a large negative exponent.
mantissa_ = 0;
exponent_ = -100;
return *this;
}
inline IOUAmount::operator Number() const
{
return Number{mantissa_, exponent_};
}
inline IOUAmount&
IOUAmount::operator-=(IOUAmount const& other)
{
*this += -other;
return *this;
}
inline IOUAmount
IOUAmount::operator-() const
{
return {-mantissa_, exponent_};
}
inline bool
IOUAmount::operator==(IOUAmount const& other) const
{
return exponent_ == other.exponent_ && mantissa_ == other.mantissa_;
}
inline bool
IOUAmount::operator<(IOUAmount const& other) const
{
return Number{*this} < Number{other};
}
inline IOUAmount::operator bool() const noexcept
{
return mantissa_ != 0;
}
inline int
IOUAmount::signum() const noexcept
{
return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0);
}
inline int
IOUAmount::exponent() const noexcept
{
return exponent_;
}
inline std::int64_t
IOUAmount::mantissa() const noexcept
{
return mantissa_;
}
std::string std::string
to_string(IOUAmount const& amount); to_string(IOUAmount const& amount);
@@ -149,6 +186,35 @@ mulRatio(
std::uint32_t den, std::uint32_t den,
bool roundUp); bool roundUp);
// Since IOUAmount and STAmount do not have access to a ledger, this
// is needed to put low-level routines on an amendment switch. Only
// transactions need to use this switchover. Outside of a transaction
// it's safe to unconditionally use the new behavior.
extern LocalValue<bool> stNumberSwitchover;
/** RAII class to set and restore the Number switchover.
*/
class NumberSO
{
bool saved_;
public:
~NumberSO()
{
*stNumberSwitchover = saved_;
}
NumberSO(NumberSO const&) = delete;
NumberSO&
operator=(NumberSO const&) = delete;
explicit NumberSO(bool v) : saved_(*stNumberSwitchover)
{
*stNumberSwitchover = v;
}
};
} // namespace ripple } // namespace ripple
#endif #endif

View File

@@ -20,7 +20,6 @@
#ifndef RIPPLE_BASICS_NUMBER_H_INCLUDED #ifndef RIPPLE_BASICS_NUMBER_H_INCLUDED
#define RIPPLE_BASICS_NUMBER_H_INCLUDED #define RIPPLE_BASICS_NUMBER_H_INCLUDED
#include <ripple/basics/IOUAmount.h>
#include <ripple/basics/XRPAmount.h> #include <ripple/basics/XRPAmount.h>
#include <cstdint> #include <cstdint>
#include <ostream> #include <ostream>
@@ -51,7 +50,6 @@ public:
explicit Number(rep mantissa, int exponent); explicit Number(rep mantissa, int exponent);
explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept;
Number(IOUAmount const& x);
Number(XRPAmount const& x); Number(XRPAmount const& x);
constexpr rep constexpr rep
@@ -82,7 +80,6 @@ public:
Number& Number&
operator/=(Number const& x); operator/=(Number const& x);
explicit operator IOUAmount() const;
explicit operator XRPAmount() const; // round to nearest, even on tie explicit operator XRPAmount() const; // round to nearest, even on tie
explicit operator rep() const; // round to nearest, even on tie explicit operator rep() const; // round to nearest, even on tie
@@ -184,10 +181,6 @@ inline Number::Number(rep mantissa) : Number{mantissa, 0}
{ {
} }
inline Number::Number(IOUAmount const& x) : Number{x.mantissa(), x.exponent()}
{
}
inline Number::Number(XRPAmount const& x) : Number{x.drops()} inline Number::Number(XRPAmount const& x) : Number{x.drops()}
{ {
} }
@@ -286,11 +279,6 @@ operator/(Number const& x, Number const& y)
return z; return z;
} }
inline Number::operator IOUAmount() const
{
return IOUAmount{mantissa(), exponent()};
}
inline constexpr bool inline constexpr bool
Number::isnormal() const noexcept Number::isnormal() const noexcept
{ {

View File

@@ -27,12 +27,14 @@
namespace ripple { namespace ripple {
LocalValue<bool> stNumberSwitchover(true);
/* The range for the mantissa when normalized */ /* The range for the mantissa when normalized */
static std::int64_t const minMantissa = 1000000000000000ull; static std::int64_t constexpr minMantissa = 1000000000000000ull;
static std::int64_t const maxMantissa = 9999999999999999ull; static std::int64_t constexpr maxMantissa = 9999999999999999ull;
/* The range for the exponent when normalized */ /* The range for the exponent when normalized */
static int const minExponent = -96; static int constexpr minExponent = -96;
static int const maxExponent = 80; static int constexpr maxExponent = 80;
IOUAmount IOUAmount
IOUAmount::minPositiveAmount() IOUAmount::minPositiveAmount()
@@ -43,6 +45,17 @@ IOUAmount::minPositiveAmount()
void void
IOUAmount::normalize() IOUAmount::normalize()
{ {
if (*stNumberSwitchover)
{
Number v{mantissa_, exponent_};
mantissa_ = v.mantissa();
exponent_ = v.exponent();
if (exponent_ > maxExponent)
Throw<std::overflow_error>("value overflow");
if (exponent_ < minExponent)
*this = beast::zero;
return;
}
if (mantissa_ == 0) if (mantissa_ == 0)
{ {
*this = beast::zero; *this = beast::zero;
@@ -82,166 +95,67 @@ IOUAmount::normalize()
mantissa_ = -mantissa_; mantissa_ = -mantissa_;
} }
IOUAmount::IOUAmount(Number const& other)
: mantissa_(other.mantissa()), exponent_(other.exponent())
{
if (exponent_ > maxExponent)
Throw<std::overflow_error>("value overflow");
if (exponent_ < minExponent)
*this = beast::zero;
}
IOUAmount& IOUAmount&
IOUAmount::operator+=(IOUAmount const& other) IOUAmount::operator+=(IOUAmount const& other)
{ {
if (other == beast::zero) if (*stNumberSwitchover)
return *this;
if (*this == beast::zero)
{ {
*this = other; *this = IOUAmount{Number{*this} + Number{other}};
return *this;
} }
else
auto m = other.mantissa_;
auto e = other.exponent_;
while (exponent_ < e)
{ {
mantissa_ /= 10; if (other == beast::zero)
++exponent_; return *this;
if (*this == beast::zero)
{
*this = other;
return *this;
}
auto m = other.mantissa_;
auto e = other.exponent_;
while (exponent_ < e)
{
mantissa_ /= 10;
++exponent_;
}
while (e < exponent_)
{
m /= 10;
++e;
}
// This addition cannot overflow an std::int64_t but we may throw from
// normalize if the result isn't representable.
mantissa_ += m;
if (mantissa_ >= -10 && mantissa_ <= 10)
{
*this = beast::zero;
return *this;
}
normalize();
} }
while (e < exponent_)
{
m /= 10;
++e;
}
// This addition cannot overflow an std::int64_t but we may throw from
// normalize if the result isn't representable.
mantissa_ += m;
if (mantissa_ >= -10 && mantissa_ <= 10)
{
*this = beast::zero;
return *this;
}
normalize();
return *this; return *this;
} }
bool
IOUAmount::operator<(IOUAmount const& other) const
{
// If the two amounts have different signs (zero is treated as positive)
// then the comparison is true iff the left is negative.
bool const lneg = mantissa_ < 0;
bool const rneg = other.mantissa_ < 0;
if (lneg != rneg)
return lneg;
// Both have same sign and the left is zero: the right must be
// greater than 0.
if (mantissa_ == 0)
return other.mantissa_ > 0;
// Both have same sign, the right is zero and the left is non-zero.
if (other.mantissa_ == 0)
return false;
// Both have the same sign, compare by exponents:
if (exponent_ > other.exponent_)
return lneg;
if (exponent_ < other.exponent_)
return !lneg;
// If equal exponents, compare mantissas
return mantissa_ < other.mantissa_;
}
std::string std::string
to_string(IOUAmount const& amount) to_string(IOUAmount const& amount)
{ {
// keep full internal accuracy, but make more human friendly if possible return to_string(Number{amount});
if (amount == beast::zero)
return "0";
int const exponent = amount.exponent();
auto mantissa = amount.mantissa();
// Use scientific notation for exponents that are too small or too large
if (((exponent != 0) && ((exponent < -25) || (exponent > -5))))
{
std::string ret = std::to_string(mantissa);
ret.append(1, 'e');
ret.append(std::to_string(exponent));
return ret;
}
bool negative = false;
if (mantissa < 0)
{
mantissa = -mantissa;
negative = true;
}
assert(exponent + 43 > 0);
size_t const pad_prefix = 27;
size_t const pad_suffix = 23;
std::string const raw_value(std::to_string(mantissa));
std::string val;
val.reserve(raw_value.length() + pad_prefix + pad_suffix);
val.append(pad_prefix, '0');
val.append(raw_value);
val.append(pad_suffix, '0');
size_t const offset(exponent + 43);
auto pre_from(val.begin());
auto const pre_to(val.begin() + offset);
auto const post_from(val.begin() + offset);
auto post_to(val.end());
// Crop leading zeroes. Take advantage of the fact that there's always a
// fixed amount of leading zeroes and skip them.
if (std::distance(pre_from, pre_to) > pad_prefix)
pre_from += pad_prefix;
assert(post_to >= post_from);
pre_from = std::find_if(pre_from, pre_to, [](char c) { return c != '0'; });
// Crop trailing zeroes. Take advantage of the fact that there's always a
// fixed amount of trailing zeroes and skip them.
if (std::distance(post_from, post_to) > pad_suffix)
post_to -= pad_suffix;
assert(post_to >= post_from);
post_to = std::find_if(
std::make_reverse_iterator(post_to),
std::make_reverse_iterator(post_from),
[](char c) { return c != '0'; })
.base();
std::string ret;
if (negative)
ret.append(1, '-');
// Assemble the output:
if (pre_from == pre_to)
ret.append(1, '0');
else
ret.append(pre_from, pre_to);
if (post_to != post_from)
{
ret.append(1, '.');
ret.append(post_from, post_to);
}
return ret;
} }
IOUAmount IOUAmount

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how // Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this. // the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 55; static constexpr std::size_t numFeatures = 56;
/** Amendments that this server supports and the default voting behavior. /** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated Whether they are enabled depends on the Rules defined in the validated
@@ -340,8 +340,12 @@ extern uint256 const featureNonFungibleTokensV1_1;
extern uint256 const fixTrustLinesToSelf; extern uint256 const fixTrustLinesToSelf;
extern uint256 const fixRemoveNFTokenAutoTrustLine; extern uint256 const fixRemoveNFTokenAutoTrustLine;
extern uint256 const featureImmediateOfferKilled; extern uint256 const featureImmediateOfferKilled;
<<<<<<< HEAD
extern uint256 const featureDisallowIncoming; extern uint256 const featureDisallowIncoming;
extern uint256 const featureXRPFees; extern uint256 const featureXRPFees;
=======
extern uint256 const fixUniversalNumber;
>>>>>>> Use Number for IOUAmount and STAmount arithmetic
} // namespace ripple } // namespace ripple

View File

@@ -277,7 +277,13 @@ private:
STBase* STBase*
move(std::size_t n, void* buf) override; move(std::size_t n, void* buf) override;
STAmount&
operator=(IOUAmount const& iou);
friend class detail::STVar; friend class detail::STVar;
friend STAmount
operator+(STAmount const& v1, STAmount const& v2);
}; };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------

View File

@@ -452,6 +452,7 @@ REGISTER_FIX (fixRemoveNFTokenAutoTrustLine, Supported::yes, DefaultVote::yes
REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no); REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no);
REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no); REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no);
REGISTER_FEATURE(XRPFees, Supported::yes, DefaultVote::no); REGISTER_FEATURE(XRPFees, Supported::yes, DefaultVote::no);
REGISTER_FIX (fixUniversalNumber, Supported::yes, DefaultVote::yes);
// The following amendments have been active for at least two years. Their // The following amendments have been active for at least two years. Their
// pre-amendment code has been removed and the identifiers are deprecated. // pre-amendment code has been removed and the identifiers are deprecated.

View File

@@ -339,6 +339,19 @@ STAmount::iou() const
return {mantissa, exponent}; return {mantissa, exponent};
} }
STAmount&
STAmount::operator=(IOUAmount const& iou)
{
assert(mIsNative == false);
mOffset = iou.exponent();
mIsNegative = iou < beast::zero;
if (mIsNegative)
mValue = static_cast<std::uint64_t>(-iou.mantissa());
else
mValue = static_cast<std::uint64_t>(iou.mantissa());
return *this;
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// //
// Operators // Operators
@@ -382,6 +395,13 @@ operator+(STAmount const& v1, STAmount const& v2)
if (v1.native()) if (v1.native())
return {v1.getFName(), getSNValue(v1) + getSNValue(v2)}; return {v1.getFName(), getSNValue(v1) + getSNValue(v2)};
if (*stNumberSwitchover)
{
auto x = v1;
x = v1.iou() + v2.iou();
return x;
}
int ov1 = v1.exponent(), ov2 = v2.exponent(); int ov1 = v1.exponent(), ov2 = v2.exponent();
std::int64_t vv1 = static_cast<std::int64_t>(v1.mantissa()); std::int64_t vv1 = static_cast<std::int64_t>(v1.mantissa());
std::int64_t vv2 = static_cast<std::int64_t>(v2.mantissa()); std::int64_t vv2 = static_cast<std::int64_t>(v2.mantissa());
@@ -733,6 +753,12 @@ STAmount::canonicalize()
mIsNative = false; mIsNative = false;
if (*stNumberSwitchover)
{
*this = iou();
return;
}
if (mValue == 0) if (mValue == 0)
{ {
mOffset = -100; mOffset = -100;
@@ -1170,6 +1196,9 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue)
return STAmount(v1.getFName(), minV * maxV); return STAmount(v1.getFName(), minV * maxV);
} }
if (*stNumberSwitchover)
return {IOUAmount{Number{v1} * Number{v2}}, issue};
std::uint64_t value1 = v1.mantissa(); std::uint64_t value1 = v1.mantissa();
std::uint64_t value2 = v2.mantissa(); std::uint64_t value2 = v2.mantissa();
int offset1 = v1.exponent(); int offset1 = v1.exponent();

View File

@@ -2122,7 +2122,7 @@ public:
jrr = ledgerEntryState(env, bob, gw, "USD"); jrr = ledgerEntryState(env, bob, gw, "USD");
BEAST_EXPECT( BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName][jss::value] == jrr[jss::node][sfBalance.fieldName][jss::value] ==
"-0.966500000033334"); "-0.9665000000333333");
} }
void void

View File

@@ -905,6 +905,7 @@ public:
{ {
testcase("IOU to IOU"); testcase("IOU to IOU");
NumberSO stNumberSO{true};
Quality q1 = get_quality("1", "1"); Quality q1 = get_quality("1", "1");
// Highly exaggerated 50% transfer rate for the input and output: // Highly exaggerated 50% transfer rate for the input and output:
@@ -937,7 +938,7 @@ public:
q1, q1,
{"4", "4"}, {"4", "4"},
"4", "4",
{"2.666666666666666", "2.666666666666666"}, {"2.666666666666667", "2.666666666666667"},
eur(), eur(),
usd(), usd(),
rate, rate,
@@ -993,7 +994,7 @@ public:
q1, q1,
{"2", "2"}, {"2", "2"},
"10", "10",
{"1.666666666666666", "1.666666666666666"}, {"1.666666666666667", "1.666666666666667"},
eur(), eur(),
usd(), usd(),
rate, rate,

View File

@@ -411,13 +411,11 @@ class TrustAndBalance_test : public beast::unit_test::suite
if (with_rate) if (with_rate)
{ {
// 65.00000000000001 is correct.
// This is result of limited precision.
env.require(balance( env.require(balance(
alice, alice,
STAmount( STAmount(
carol["USD"].issue(), carol["USD"].issue(),
6500000000000001ull, 6500000000000000ull,
-14, -14,
false, false,
true, true,