mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-05 17:56:49 +00:00
Compare commits
17 Commits
dangell7/d
...
vvysokikh/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1670f5614 | ||
|
|
6056045f2e | ||
|
|
a489708326 | ||
|
|
8b6b075397 | ||
|
|
cdcae49fdb | ||
|
|
763bba2aba | ||
|
|
35521e1065 | ||
|
|
7f2d18f99e | ||
|
|
4ca1c6d97f | ||
|
|
52c6652a56 | ||
|
|
7ed000495c | ||
|
|
89c38e6220 | ||
|
|
00d46c5423 | ||
|
|
7e15621e7b | ||
|
|
99431d7833 | ||
|
|
47365f4220 | ||
|
|
1599c1a672 |
@@ -953,6 +953,21 @@
|
||||
#
|
||||
# Optional keys for NuDB and RocksDB:
|
||||
#
|
||||
# cache_size Size of cache for database records. Default is 16384.
|
||||
# Setting this value to 0 will use the default value.
|
||||
#
|
||||
# cache_age Length of time in minutes to keep database records
|
||||
# cached. Default is 5 minutes. Setting this value to
|
||||
# 0 will use the default value.
|
||||
#
|
||||
# Note: if cache_size or cache_age is not specified,
|
||||
# default values will be used for the unspecified
|
||||
# parameter.
|
||||
#
|
||||
# Note: the cache will not be created if online_delete
|
||||
# is specified, because the rotating NodeStore does
|
||||
# not use this cache).
|
||||
#
|
||||
# fast_load Boolean. If set, load the last persisted ledger
|
||||
# from disk upon process start before syncing to
|
||||
# the network. This is likely to improve performance
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
|
||||
#include <array>
|
||||
#include <compare>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -40,6 +43,47 @@ isPowerOfTen(T value)
|
||||
return logTen(value).has_value();
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
/** Builds a table of the powers of 10
|
||||
*
|
||||
* This function is marked consteval, so it can only be run in
|
||||
* a constexpr context. This assures that it is and can only be run at
|
||||
* compile time. Doing it at runtime would be pretty wasteful and
|
||||
* inefficient.
|
||||
*/
|
||||
constexpr std::size_t kInt64Digits = 20;
|
||||
consteval std::array<std::uint64_t, kInt64Digits>
|
||||
buildPowersOfTen()
|
||||
{
|
||||
std::array<std::uint64_t, kInt64Digits> result{};
|
||||
|
||||
std::uint64_t power = 1;
|
||||
std::size_t exponent = 0;
|
||||
// end the loop early so it doesn't overflow;
|
||||
for (; exponent < result.size() - 1; ++exponent, power *= 10)
|
||||
{
|
||||
result[exponent] = power;
|
||||
if (power > std::numeric_limits<std::uint64_t>::max() / 10)
|
||||
throw std::logic_error("Power of 10 table is too big");
|
||||
}
|
||||
result[exponent] = power;
|
||||
if (power < std::numeric_limits<std::uint64_t>::max() / 10)
|
||||
throw std::logic_error("Power of 10 table is not big enough for the uint64_t type");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
constexpr std::array<std::uint64_t, detail::kInt64Digits> kPowerOfTen = detail::buildPowersOfTen();
|
||||
|
||||
static_assert(kPowerOfTen[0] == 1);
|
||||
static_assert(kPowerOfTen[1] == 10);
|
||||
static_assert(kPowerOfTen[10] == 10'000'000'000);
|
||||
static_assert(
|
||||
isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kInt64Digits - 1);
|
||||
|
||||
/** MantissaRange defines a range for the mantissa of a normalized Number.
|
||||
*
|
||||
* The mantissa is in the range [min, max], where
|
||||
@@ -76,6 +120,7 @@ isPowerOfTen(T value)
|
||||
struct MantissaRange final
|
||||
{
|
||||
using rep = std::uint64_t;
|
||||
|
||||
enum class MantissaScale {
|
||||
Small,
|
||||
// LargeLegacy can be removed when fixCleanup3_2_0 is retired
|
||||
@@ -89,19 +134,15 @@ struct MantissaRange final
|
||||
Enabled = true,
|
||||
};
|
||||
|
||||
explicit constexpr MantissaRange(MantissaScale scale)
|
||||
: min(getMin(scale))
|
||||
, cuspRoundingFixEnabled(isCuspFixEnabled(scale))
|
||||
, log(logTen(min).value_or(-1))
|
||||
, scale(scale)
|
||||
explicit constexpr MantissaRange(MantissaScale sc) : scale(sc)
|
||||
{
|
||||
}
|
||||
|
||||
rep min;
|
||||
rep max{(min * 10) - 1};
|
||||
CuspRoundingFix cuspRoundingFixEnabled;
|
||||
int log;
|
||||
MantissaScale scale;
|
||||
MantissaScale const scale;
|
||||
int const log{getExponent(scale)};
|
||||
rep const min{getMin(scale, log)};
|
||||
rep const max{(min * 10) - 1};
|
||||
CuspRoundingFix const cuspRoundingFixEnabled{isCuspFixEnabled(scale)};
|
||||
|
||||
static MantissaRange const&
|
||||
getMantissaRange(MantissaScale scale);
|
||||
@@ -110,23 +151,35 @@ struct MantissaRange final
|
||||
getAllScales();
|
||||
|
||||
private:
|
||||
static constexpr rep
|
||||
getMin(MantissaScale scale)
|
||||
static constexpr int
|
||||
getExponent(MantissaScale scale)
|
||||
{
|
||||
switch (scale)
|
||||
{
|
||||
case MantissaScale::Small:
|
||||
return 1'000'000'000'000'000ULL;
|
||||
return 15;
|
||||
case MantissaScale::LargeLegacy:
|
||||
case MantissaScale::Large:
|
||||
return 1'000'000'000'000'000'000ULL;
|
||||
return 18;
|
||||
// LCOV_EXCL_START
|
||||
default:
|
||||
// If called in a constexpr context, this throw assures that the build fails if an
|
||||
// invalid scale is used.
|
||||
throw std::runtime_error("Unknown mantissa scale"); // LCOV_EXCL_LINE
|
||||
throw std::runtime_error("Unknown mantissa scale");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
// Keep this function for future use with different ways to compute
|
||||
// the ranges.
|
||||
static constexpr rep
|
||||
getMin(MantissaScale scale, int exponent)
|
||||
{
|
||||
if (exponent < 0 || exponent >= kPowerOfTen.size())
|
||||
throw std::runtime_error("Invalid exponent"); // LCOV_EXCL_LINE
|
||||
return kPowerOfTen[exponent];
|
||||
}
|
||||
|
||||
static constexpr CuspRoundingFix
|
||||
isCuspFixEnabled(MantissaScale scale)
|
||||
{
|
||||
@@ -349,40 +402,41 @@ public:
|
||||
x.exponent_ == y.exponent_;
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator!=(Number const& x, Number const& y) noexcept
|
||||
// operator!=, >, <=, >= are synthesized from operator== and operator<=>.
|
||||
friend constexpr std::strong_ordering
|
||||
operator<=>(Number const& l, Number const& r) noexcept
|
||||
{
|
||||
return !(x == y);
|
||||
}
|
||||
bool const lneg = l.negative_;
|
||||
bool const rneg = r.negative_;
|
||||
|
||||
friend constexpr bool
|
||||
operator<(Number const& x, Number const& y) noexcept
|
||||
{
|
||||
// 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 = x.negative_;
|
||||
bool const rneg = y.negative_;
|
||||
|
||||
// then the negative one is smaller.
|
||||
if (lneg != rneg)
|
||||
return lneg;
|
||||
{
|
||||
return lneg ? std::strong_ordering::less : std::strong_ordering::greater;
|
||||
}
|
||||
|
||||
// Both have same sign and the left is zero: the right must be
|
||||
// greater than 0.
|
||||
if (x.mantissa_ == 0)
|
||||
return y.mantissa_ > 0;
|
||||
// Same sign: compare the unsigned magnitudes |a| <=> |b|. For negative
|
||||
// values the order is reversed (larger magnitude == smaller value), so
|
||||
// swap the operands. A negative value is never zero, so the zero checks
|
||||
// below only ever fire for the non-negative case.
|
||||
Number const& a = lneg ? r : l;
|
||||
Number const& b = lneg ? l : r;
|
||||
|
||||
// Both have same sign, the right is zero and the left is non-zero.
|
||||
if (y.mantissa_ == 0)
|
||||
return false;
|
||||
// A zero mantissa carries a sentinel exponent, so zero has to be handled
|
||||
// before the exponents can be compared.
|
||||
if (a.mantissa_ == 0)
|
||||
{
|
||||
return b.mantissa_ == 0 ? std::strong_ordering::equal : std::strong_ordering::less;
|
||||
}
|
||||
if (b.mantissa_ == 0)
|
||||
return std::strong_ordering::greater;
|
||||
|
||||
// Both have the same sign, compare by exponents:
|
||||
if (x.exponent_ > y.exponent_)
|
||||
return lneg;
|
||||
if (x.exponent_ < y.exponent_)
|
||||
return !lneg;
|
||||
|
||||
// If equal exponents, compare mantissas
|
||||
return x.mantissa_ < y.mantissa_;
|
||||
// Both are non-zero and normalized, so the exponent dominates and the
|
||||
// mantissa breaks ties.
|
||||
if (auto const cmp = a.exponent_ <=> b.exponent_; cmp != 0)
|
||||
return cmp;
|
||||
return a.mantissa_ <=> b.mantissa_;
|
||||
}
|
||||
|
||||
/** Return the sign of the amount */
|
||||
@@ -397,24 +451,6 @@ public:
|
||||
[[nodiscard]] Number
|
||||
truncate() const noexcept;
|
||||
|
||||
friend constexpr bool
|
||||
operator>(Number const& x, Number const& y) noexcept
|
||||
{
|
||||
return y < x;
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator<=(Number const& x, Number const& y) noexcept
|
||||
{
|
||||
return !(y < x);
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator>=(Number const& x, Number const& y) noexcept
|
||||
{
|
||||
return !(x < y);
|
||||
}
|
||||
|
||||
friend std::ostream&
|
||||
operator<<(std::ostream& os, Number const& x)
|
||||
{
|
||||
@@ -477,8 +513,7 @@ public:
|
||||
template <
|
||||
auto MinMantissa,
|
||||
auto MaxMantissa,
|
||||
Integral64 T = std::decay_t<decltype(MinMantissa)>,
|
||||
Integral64 TMax = std::decay_t<decltype(MaxMantissa)>>
|
||||
Integral64 T = std::decay_t<decltype(MinMantissa)>>
|
||||
[[nodiscard]]
|
||||
std::pair<T, int>
|
||||
normalizeToRange() const;
|
||||
@@ -519,7 +554,8 @@ private:
|
||||
int& exponent,
|
||||
MantissaRange::rep const& minMantissa,
|
||||
MantissaRange::rep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled);
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
|
||||
bool dropped);
|
||||
|
||||
[[nodiscard]] bool
|
||||
isnormal() const noexcept;
|
||||
@@ -725,16 +761,18 @@ Number::isnormal() const noexcept
|
||||
kMinExponent <= exponent_ && exponent_ <= kMaxExponent);
|
||||
}
|
||||
|
||||
template <auto MinMantissa, auto MaxMantissa, Integral64 T, Integral64 TMax>
|
||||
template <auto MinMantissa, auto MaxMantissa, Integral64 T>
|
||||
std::pair<T, int>
|
||||
Number::normalizeToRange() const
|
||||
{
|
||||
static_assert(std::is_same_v<T, std::uint64_t> || std::is_same_v<T, std::int64_t>);
|
||||
static_assert(std::is_same_v<T, TMax>);
|
||||
static_assert(std::is_same_v<T, std::decay_t<decltype(MinMantissa)>>);
|
||||
static_assert(std::is_same_v<T, std::decay_t<decltype(MaxMantissa)>>);
|
||||
auto constexpr kMIN = static_cast<T>(MinMantissa);
|
||||
auto constexpr kMAX = static_cast<T>(MaxMantissa);
|
||||
static_assert(kMIN > 0);
|
||||
static_assert(kMIN % 10 == 0);
|
||||
static_assert(isPowerOfTen(kMIN));
|
||||
static_assert(kMAX % 10 == 9);
|
||||
static_assert((kMAX + 1) / 10 == kMIN);
|
||||
|
||||
|
||||
@@ -461,6 +461,7 @@ loanAccruedInterest(
|
||||
|
||||
ExtendedPaymentComponents
|
||||
computeOverpaymentComponents(
|
||||
Rules const& rules,
|
||||
Asset const& asset,
|
||||
int32_t const loanScale,
|
||||
Number const& overpayment,
|
||||
|
||||
@@ -131,6 +131,10 @@ public:
|
||||
std::uint32_t ledgerSeq,
|
||||
std::function<void(std::shared_ptr<NodeObject> const&)>&& callback);
|
||||
|
||||
/** Remove expired entries from the positive and negative caches. */
|
||||
virtual void
|
||||
sweep() = 0;
|
||||
|
||||
/** Gather statistics pertaining to read and write activities.
|
||||
*
|
||||
* @param obj Json object reference into which to place counters.
|
||||
|
||||
@@ -22,6 +22,32 @@ public:
|
||||
beast::Journal j)
|
||||
: Database(scheduler, readThreads, config, j), backend_(std::move(backend))
|
||||
{
|
||||
std::optional<int> cacheSize, cacheAge;
|
||||
|
||||
if (config.exists("cache_size"))
|
||||
{
|
||||
cacheSize = get<int>(config, "cache_size");
|
||||
if (cacheSize.value() < 0)
|
||||
Throw<std::runtime_error>("Specified negative value for cache_size");
|
||||
}
|
||||
|
||||
if (config.exists("cache_age"))
|
||||
{
|
||||
cacheAge = get<int>(config, "cache_age");
|
||||
if (cacheAge.value() < 0)
|
||||
Throw<std::runtime_error>("Specified negative value for cache_age");
|
||||
}
|
||||
|
||||
if (cacheSize.has_value() || cacheAge.has_value())
|
||||
{
|
||||
cache_ = std::make_shared<TaggedCache<uint256, NodeObject>>(
|
||||
"DatabaseNodeImp",
|
||||
cacheSize.value_or(0),
|
||||
std::chrono::minutes(cacheAge.value_or(0)),
|
||||
stopwatch(),
|
||||
j);
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
backend_,
|
||||
"xrpl::NodeStore::DatabaseNodeImp::DatabaseNodeImp : non-null "
|
||||
@@ -73,7 +99,13 @@ public:
|
||||
std::uint32_t ledgerSeq,
|
||||
std::function<void(std::shared_ptr<NodeObject> const&)>&& callback) override;
|
||||
|
||||
void
|
||||
sweep() override;
|
||||
|
||||
private:
|
||||
// Cache for database objects. This cache is not always initialized. Check
|
||||
// for null before using.
|
||||
std::shared_ptr<TaggedCache<uint256, NodeObject>> cache_;
|
||||
// Persistent key/value storage
|
||||
std::shared_ptr<Backend> backend_;
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ public:
|
||||
void
|
||||
sync() override;
|
||||
|
||||
void
|
||||
sweep() override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Backend> writableBackend_;
|
||||
std::shared_ptr<Backend> archiveBackend_;
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -74,18 +72,6 @@ class NetworkOPs : public InfoSub::Source
|
||||
public:
|
||||
using clock_type = beast::AbstractClock<std::chrono::steady_clock>;
|
||||
|
||||
// Snapshot of per-operating-mode accounting, exposed for the datagram monitor.
|
||||
struct AccountingCounter
|
||||
{
|
||||
std::uint64_t transitions{0};
|
||||
std::chrono::microseconds dur{std::chrono::microseconds(0)};
|
||||
};
|
||||
using StateAccountingData = std::tuple<
|
||||
std::array<AccountingCounter, 5>,
|
||||
OperatingMode,
|
||||
std::chrono::steady_clock::time_point,
|
||||
std::uint64_t>;
|
||||
|
||||
enum class FailHard : unsigned char { No, Yes };
|
||||
static FailHard
|
||||
doFailHard(bool noMeansDont)
|
||||
@@ -106,8 +92,6 @@ public:
|
||||
|
||||
[[nodiscard]] virtual OperatingMode
|
||||
getOperatingMode() const = 0;
|
||||
[[nodiscard]] virtual StateAccountingData
|
||||
getStateAccountingData() = 0;
|
||||
[[nodiscard]] virtual std::string
|
||||
strOperatingMode(OperatingMode const mode, bool const admin = false) const = 0;
|
||||
[[nodiscard]] virtual std::string
|
||||
|
||||
@@ -178,6 +178,10 @@ public:
|
||||
setPositive() noexcept;
|
||||
void
|
||||
setNegative() noexcept;
|
||||
// Should only be called by doNormalize, and then only for division
|
||||
// operations with remainders.
|
||||
void
|
||||
setDropped() noexcept;
|
||||
[[nodiscard]] bool
|
||||
isNegative() const noexcept;
|
||||
|
||||
@@ -250,6 +254,12 @@ Number::Guard::setNegative() noexcept
|
||||
sbit_ = 1;
|
||||
}
|
||||
|
||||
inline void
|
||||
Number::Guard::setDropped() noexcept
|
||||
{
|
||||
xbit_ = 1;
|
||||
}
|
||||
|
||||
inline bool
|
||||
Number::Guard::isNegative() const noexcept
|
||||
{
|
||||
@@ -398,7 +408,7 @@ Number::Guard::doRoundUp(
|
||||
// _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.
|
||||
// chance of bringing it back over.
|
||||
doDropDigit(mantissa, exponent);
|
||||
XRPL_ASSERT_PARTS(
|
||||
safeToIncrement(mantissa),
|
||||
@@ -512,8 +522,6 @@ Number::one()
|
||||
return Number{false, range.min, -range.log, Number::Unchecked{}};
|
||||
}
|
||||
|
||||
// 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>
|
||||
void
|
||||
doNormalize(
|
||||
@@ -522,7 +530,8 @@ doNormalize(
|
||||
int& exponent,
|
||||
MantissaRange::rep const& minMantissa,
|
||||
MantissaRange::rep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
|
||||
bool dropped)
|
||||
{
|
||||
static constexpr auto kMinExponent = Number::kMinExponent;
|
||||
static constexpr auto kMaxExponent = Number::kMaxExponent;
|
||||
@@ -547,6 +556,8 @@ doNormalize(
|
||||
Guard g;
|
||||
if (negative)
|
||||
g.setNegative();
|
||||
if (dropped)
|
||||
g.setDropped();
|
||||
while (m > maxMantissa)
|
||||
{
|
||||
if (exponent >= kMaxExponent)
|
||||
@@ -611,7 +622,12 @@ Number::normalize<uint128_t>(
|
||||
internalrep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
|
||||
{
|
||||
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled);
|
||||
// Not used by every compiler version, and thus not necessarily
|
||||
// counted by coverage build
|
||||
// LCOV_EXCL_START
|
||||
doNormalize(
|
||||
negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -624,7 +640,12 @@ Number::normalize<unsigned long long>(
|
||||
internalrep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
|
||||
{
|
||||
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled);
|
||||
// Not used by every compiler version, and thus not necessarily
|
||||
// counted by coverage build
|
||||
// LCOV_EXCL_START
|
||||
doNormalize(
|
||||
negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -637,7 +658,8 @@ Number::normalize<unsigned long>(
|
||||
internalrep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
|
||||
{
|
||||
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled);
|
||||
doNormalize(
|
||||
negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -838,7 +860,9 @@ Number::operator/=(Number const& y)
|
||||
return *this;
|
||||
// n* = numerator
|
||||
// d* = denominator
|
||||
// *p = negative (positive?)
|
||||
// z* = result (quotient)
|
||||
// *p = negative (p for positive, even though the value means not
|
||||
// positive?)
|
||||
// *s = sign
|
||||
// *m = mantissa
|
||||
// *e = exponent
|
||||
@@ -850,71 +874,155 @@ Number::operator/=(Number const& y)
|
||||
|
||||
bool const dp = y.negative_;
|
||||
int const ds = (dp ? -1 : 1);
|
||||
auto dm = y.mantissa_;
|
||||
auto de = y.exponent_;
|
||||
// Create the denominator as 128-bit unsigned, since that's what we
|
||||
// need to work with.
|
||||
uint128_t const dm = static_cast<uint128_t>(y.mantissa_);
|
||||
auto const de = y.exponent_;
|
||||
|
||||
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
|
||||
// TODO: Can/should this be made bigger for largeRange?
|
||||
// log(2^128,10) ~ 38.5
|
||||
// largeRange.log = 18, fits in 10^19
|
||||
// f can be up to 10^(38-19) = 10^19 safely
|
||||
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");
|
||||
// Division operates on two large integers (16-digit for small
|
||||
// mantissas, 19-digit for large) using integer math. If the values
|
||||
// were just divided directly, the result would be only ever be one
|
||||
// digit or zero - not very useful.
|
||||
// e.g. 9'876'543'210'987'654 / 1'234'567'890'123'456 = 8
|
||||
// 1'234'567'890'123'456 / 9'876'543'210'987'654 = 0
|
||||
// Introduce a power-of-ten multiplication factor for the numerator
|
||||
// which will ensure the result has a meaningful number of digits.
|
||||
//
|
||||
// Consider numbers with a 2-digit mantissa:
|
||||
// * Assume both numbers have an exponent of 0, using "ToNearest" rounding
|
||||
// * 23 / 67 = 0
|
||||
// * Use a factor of 10^4
|
||||
// * 230'000 / 67 = 3432 with an exponent of -4
|
||||
// * The normalized result will be 34, exponent -2, or 0.34
|
||||
//
|
||||
// The most extreme results are 10/99 and 99/10
|
||||
// * 100'000 / 99 = 1'010e-4 = 10e-2 or 0.10
|
||||
// * 990'000 / 10 = 99'000e-4 = 99e-1 or 9.9
|
||||
//
|
||||
// Note that the computations give 2 or 3 digits after the
|
||||
// decimal point to determine which way to round for most scenarios.
|
||||
//
|
||||
// For small mantissas (where the MantissaRange.log == 15), shifting by 10^17 gives sufficient
|
||||
// precision while not overflowing uint128_t or the cast back to int64_t. (This is legacy
|
||||
// behavior, which must not be changed.)
|
||||
//
|
||||
// For large mantissas (where the MantissaRange.log == 18), a shift by 10^20 would be optimal
|
||||
// for most scenarios. However, larger mantissa values would overflow 2^128.
|
||||
//
|
||||
// * log(2^128,10) ~ 38.5
|
||||
// * largeRange.log = 18, fits in 10^19
|
||||
// * The expanded numerator must fit in 10^38
|
||||
// * f not be more than 10^(38-19) = 10^19 safely
|
||||
//
|
||||
// So, we do the division into stages:
|
||||
//
|
||||
// Stage 1: Use the same factor of 10^17, for the initial division. This
|
||||
// will frequently not result in a whole number quotient.
|
||||
//
|
||||
// Stage 2: If there is a remainder from the first step, repeat the
|
||||
// process with a "correction" factor of 10^5. Shift the
|
||||
// result of Stage 1 over by 5 places, and add the second result to it.
|
||||
// This is equivalent to if we had used an initial factor of 10^22,
|
||||
// a couple digits more than we actually need.
|
||||
//
|
||||
// Stage 3: If there is still a remainder, and the CuspRoundingFix
|
||||
// is enabled, pass a flag indicating such to doNormalize. The Guard
|
||||
// in doNormalize will treat that flag as if non-zero digits had
|
||||
// been dropped from the mantissa when shrinking it into range.
|
||||
// This is only relevant when rounding away from zero (Upward for
|
||||
// positive numbers, Downward for negative), or if the "regular"
|
||||
// remainder is exactly 0.5 for "ToNearest". This will give the
|
||||
// rounding the most accurate result possible, as if infinite
|
||||
// precision was used in the initial calculation.
|
||||
|
||||
// unsigned denominator
|
||||
auto const dmu = static_cast<uint128_t>(dm);
|
||||
// correctionFactor can be anything between 10 and f, depending on how much
|
||||
// extra precision we want to only use for rounding with the
|
||||
// largeRange. Three digits seems like plenty, and is more than
|
||||
// the smallRange uses.
|
||||
uint128_t const correctionFactor = 1'000;
|
||||
// Stage 1: Do the initial division with a factor of 10^17.
|
||||
auto constexpr factorExponent = 17;
|
||||
|
||||
uint128_t constexpr f = kPowerOfTen[factorExponent];
|
||||
|
||||
auto const numerator = uint128_t(nm) * f;
|
||||
|
||||
auto zm = numerator / dmu;
|
||||
auto ze = ne - de - (small ? 17 : 19);
|
||||
bool zn = (ns * ds) < 0;
|
||||
if (!small)
|
||||
auto zm = numerator / dm;
|
||||
auto ze = ne - de - factorExponent;
|
||||
bool zp = (ns * ds) < 0;
|
||||
// dropped is used in the same way as Guard::xbit_. In the case of
|
||||
// division, it indicates if there's any remainder left over after
|
||||
// we have been as precise as reasonable. If there is, it would be as
|
||||
// if we were using infinite precision math, and a non-zero digit
|
||||
// had been shifted off the end of the result when normalizing.
|
||||
bool dropped = false;
|
||||
|
||||
if (range.scale != MantissaRange::MantissaScale::Small)
|
||||
{
|
||||
// Virtually multiply numerator by correctionFactor. Since that would
|
||||
// overflow in the existing uint128_t, we'll do that part separately.
|
||||
// Stage 2
|
||||
//
|
||||
// If there is a remainder, treat it as a secondary numerator.
|
||||
// Multiply by correctionFactor separately from stage 1.
|
||||
// The math for this would work for small mantissas, but we need to
|
||||
// preserve existing behavior.
|
||||
// preserve legacy behavior.
|
||||
//
|
||||
// Consider:
|
||||
// ((numerator * correctionFactor) / dmu) / correctionFactor
|
||||
// = ((numerator / dmu) * correctionFactor) / correctionFactor)
|
||||
// ((numerator * correctionFactor) / dm) / correctionFactor
|
||||
// = ((numerator / dm) * correctionFactor) / correctionFactor)
|
||||
//
|
||||
// But that assumes infinite precision. With integer math, this is
|
||||
// equivalent to
|
||||
//
|
||||
// = ((numerator / dmu * correctionFactor)
|
||||
// + ((numerator % dmu) * correctionFactor) / dmu) / correctionFactor
|
||||
// = ((numerator / dm * correctionFactor)
|
||||
// + ((numerator % dm) * correctionFactor) / dm) / correctionFactor
|
||||
// = ((zm * correctionFactor)
|
||||
// + (remainder * correctionFactor) / dm) / correctionFactor
|
||||
//
|
||||
// We have already set `mantissa_ = numerator / dmu`. Now we
|
||||
// compute `remainder = numerator % dmu`, and if it is
|
||||
// nonzero, we do the rest of the arithmetic. If it's zero, we can skip
|
||||
// it.
|
||||
auto const remainder = (numerator % dmu);
|
||||
// The trick is that multiplication by correctionFactor is done on the mantissa, but
|
||||
// division by correctionFactor is done by modifying the exponent, so no precision is lost
|
||||
// until we normalize.
|
||||
//
|
||||
// If remainder is zero, we can skip this stage entirely because
|
||||
// the first stage gave an exact answer.
|
||||
auto constexpr correctionExponent = 5;
|
||||
uint128_t constexpr correctionFactor = kPowerOfTen[correctionExponent];
|
||||
static_assert(factorExponent + correctionExponent == 22);
|
||||
|
||||
auto const remainder = (numerator % dm);
|
||||
if (remainder != 0)
|
||||
{
|
||||
zm *= correctionFactor;
|
||||
auto const correction = remainder * correctionFactor / dmu;
|
||||
zm += correction;
|
||||
// divide by 1000 by moving the exponent, so we don't lose the
|
||||
// integer value we just computed
|
||||
ze -= 3;
|
||||
auto const partialNumerator = remainder * correctionFactor;
|
||||
auto const correction = partialNumerator / dm;
|
||||
|
||||
// If the correction is zero, we do not have to make any
|
||||
// modifications to z*, because it will not have any
|
||||
// effect on the final result. (We'd be adding a bunch of
|
||||
// zeros to the end of zm that would just be removed in
|
||||
// normalize.) However, if that is the case, then Stage 3 is
|
||||
// even more important for accuracy.
|
||||
if (correction != 0)
|
||||
{
|
||||
zm *= correctionFactor;
|
||||
// divide by the correctionFactor by moving the exponent, so we don't lose the
|
||||
// integer value we just computed
|
||||
ze -= correctionExponent;
|
||||
|
||||
zm += correction;
|
||||
}
|
||||
|
||||
// Stage 3: If there's still anything left, and the cusp
|
||||
// rounding fix is enabled, flag if there is still
|
||||
// a remainder from stage 2.
|
||||
bool const useTrailingRemainder =
|
||||
cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled;
|
||||
if (useTrailingRemainder)
|
||||
{
|
||||
dropped = partialNumerator % dm != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
normalize(zn, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled);
|
||||
negative_ = zn;
|
||||
doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped);
|
||||
negative_ = zp;
|
||||
mantissa_ = static_cast<internalrep>(zm);
|
||||
exponent_ = ze;
|
||||
XRPL_ASSERT_PARTS(isnormal(), "xrpl::Number::operator/=", "result is normalized");
|
||||
|
||||
@@ -559,15 +559,34 @@ tryOverpayment(
|
||||
<< ", new total value: " << newLoanProperties.loanState.valueOutstanding
|
||||
<< ", first payment principal: " << newLoanProperties.firstPaymentPrincipal;
|
||||
|
||||
// Calculate what the new loan state should be with the new periodic payment
|
||||
// including rounding errors
|
||||
auto const newTheoreticalState = computeTheoreticalLoanState(
|
||||
rules,
|
||||
newLoanProperties.periodicPayment,
|
||||
periodicRate,
|
||||
paymentRemaining,
|
||||
managementFeeRate) +
|
||||
errors;
|
||||
// Calculate what the new loan state should be with the new periodic payment,
|
||||
// including the preserved rounding errors.
|
||||
|
||||
auto const newTheoreticalState = [&]() {
|
||||
auto const state = computeTheoreticalLoanState(
|
||||
rules,
|
||||
newLoanProperties.periodicPayment,
|
||||
periodicRate,
|
||||
paymentRemaining,
|
||||
managementFeeRate) +
|
||||
errors;
|
||||
|
||||
if (!rules.enabled(fixCleanup3_2_0))
|
||||
return state;
|
||||
|
||||
// The new principal is known exactly: it is reduced by the overpayment's
|
||||
// principal portion. computeTheoreticalLoanState instead derives the
|
||||
// principal -- and, from it, the management fee and interest -- via a
|
||||
// lossy (P * factor) / factor round-trip. Pin the principal to the exact
|
||||
// value and re-derive the management fee from the exact interest gross
|
||||
// (value - principal), so the intermediate state is fully consistent with
|
||||
// the exact principal rather than the one-scale-unit-high round-trip.
|
||||
Number const principal =
|
||||
roundedOldState.principalOutstanding - overpaymentComponents.trackedPrincipalDelta;
|
||||
Number const managementFee =
|
||||
tenthBipsOfValue(state.valueOutstanding - principal, managementFeeRate);
|
||||
return constructLoanState(state.valueOutstanding, principal, managementFee);
|
||||
}();
|
||||
|
||||
JLOG(j.debug()) << "new theoretical value: " << newTheoreticalState.valueOutstanding
|
||||
<< ", principal: " << newTheoreticalState.principalOutstanding
|
||||
@@ -762,13 +781,6 @@ doOverpayment(
|
||||
// The proxies still hold the original (pre-overpayment) values, which
|
||||
// allows us to compute deltas and verify they match what we expect
|
||||
// from the overpaymentComponents and loanPaymentParts.
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedPrincipalDelta ==
|
||||
principalOutstandingProxy - newRoundedLoanState.principalOutstanding,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal change agrees");
|
||||
|
||||
JLOG(j.debug()) << "valueChange: " << loanPaymentParts.valueChange
|
||||
<< ", totalValue before: " << *totalValueOutstandingProxy
|
||||
<< ", totalValue after: " << newRoundedLoanState.valueOutstanding
|
||||
@@ -780,35 +792,50 @@ doOverpayment(
|
||||
<< overpaymentComponents.trackedPrincipalDelta -
|
||||
(totalValueOutstandingProxy - newRoundedLoanState.valueOutstanding);
|
||||
|
||||
// The valueChange returned by tryOverpayment satisfies
|
||||
// valueChange = (newInterestDue - oldInterestDue) + untrackedInterest.
|
||||
// Using the loan-state identity v = p + i + m and the adjacent
|
||||
// `principal change agrees` assertion (dp = oldP - newP), this
|
||||
// rearranges into three independently-computable terms:
|
||||
//
|
||||
// 1. TVO change beyond what principal repayment alone explains:
|
||||
// newTVO - (oldTVO - dp)
|
||||
// 2. Management fee released by re-amortization (positive when
|
||||
// mfee decreased; zero when managementFeeRate == 0):
|
||||
// oldMfee - newMfee
|
||||
// 3. The overpayment's penalty interest part (= untrackedInterest
|
||||
// for the overpayment path; see computeOverpaymentComponents):
|
||||
// trackedInterestPart()
|
||||
[[maybe_unused]] Number const tvoChange = newRoundedLoanState.valueOutstanding -
|
||||
(totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta);
|
||||
[[maybe_unused]] Number const managementFeeReleased =
|
||||
managementFeeOutstandingProxy - newRoundedLoanState.managementFeeDue;
|
||||
[[maybe_unused]] Number const interestPart = overpaymentComponents.trackedInterestPart();
|
||||
// The three assertions below are invariants that only hold once
|
||||
// fixCleanup3_2_0 pins the new principal to the exact reduction
|
||||
// (oldPrincipal - trackedPrincipalDelta). Before the amendment, the lossy
|
||||
// (P * factor) / factor round-trip can leave the new principal one
|
||||
// scale-unit high, so these equalities do not hold on the pre-amendment
|
||||
// code path and must be gated to match the fix they verify.
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
{
|
||||
// The valueChange returned by tryOverpayment satisfies
|
||||
// valueChange = (newInterestDue - oldInterestDue) + untrackedInterest.
|
||||
// Using the loan-state identity v = p + i + m and the adjacent
|
||||
// `principal change agrees` assertion (dp = oldP - newP), this
|
||||
// rearranges into three independently-computable terms:
|
||||
//
|
||||
// 1. TVO change beyond what principal repayment alone explains:
|
||||
// newTVO - (oldTVO - dp)
|
||||
// 2. Management fee released by re-amortization (positive when
|
||||
// mfee decreased; zero when managementFeeRate == 0):
|
||||
// oldMfee - newMfee
|
||||
// 3. The overpayment's penalty interest part (= untrackedInterest
|
||||
// for the overpayment path; see computeOverpaymentComponents):
|
||||
// trackedInterestPart()
|
||||
[[maybe_unused]] Number const tvoChange = newRoundedLoanState.valueOutstanding -
|
||||
(totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta);
|
||||
[[maybe_unused]] Number const managementFeeReleased =
|
||||
managementFeeOutstandingProxy - newRoundedLoanState.managementFeeDue;
|
||||
[[maybe_unused]] Number const interestPart = overpaymentComponents.trackedInterestPart();
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
loanPaymentParts.valueChange == tvoChange + managementFeeReleased + interestPart,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"interest paid agrees");
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedPrincipalDelta ==
|
||||
principalOutstandingProxy - newRoundedLoanState.principalOutstanding,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal change agrees");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedPrincipalDelta == loanPaymentParts.principalPaid,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal payment matches");
|
||||
XRPL_ASSERT_PARTS(
|
||||
loanPaymentParts.valueChange == tvoChange + managementFeeReleased + interestPart,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"interest paid agrees");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedPrincipalDelta == loanPaymentParts.principalPaid,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal payment matches");
|
||||
}
|
||||
|
||||
// All validations passed, so update the proxy objects (which will
|
||||
// modify the actual Loan ledger object)
|
||||
@@ -1144,11 +1171,13 @@ computePaymentComponents(
|
||||
// Cap each component to never exceed what's actually outstanding
|
||||
deltas.principal = std::min(deltas.principal, currentLedgerState.principalOutstanding);
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
deltas.interest <= currentLedgerState.interestDue,
|
||||
"xrpl::detail::computePaymentComponents",
|
||||
"interest due delta not greater than outstanding");
|
||||
|
||||
if (fixCleanup320Enabled)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
deltas.interest <= currentLedgerState.interestDue,
|
||||
"xrpl::detail::computePaymentComponents",
|
||||
"interest due delta not greater than outstanding");
|
||||
}
|
||||
// Cap interest to both the outstanding amount AND what's left of the
|
||||
// periodic payment after principal is paid
|
||||
deltas.interest = std::min(
|
||||
@@ -1289,6 +1318,7 @@ computePaymentComponents(
|
||||
*/
|
||||
ExtendedPaymentComponents
|
||||
computeOverpaymentComponents(
|
||||
Rules const& rules,
|
||||
Asset const& asset,
|
||||
int32_t const loanScale,
|
||||
Number const& overpayment,
|
||||
@@ -1296,10 +1326,13 @@ computeOverpaymentComponents(
|
||||
TenthBips32 const overpaymentFeeRate,
|
||||
TenthBips16 const managementFeeRate)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
overpayment > 0 && isRounded(asset, overpayment, loanScale),
|
||||
"xrpl::detail::computeOverpaymentComponents : valid overpayment "
|
||||
"amount");
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
overpayment > 0 && isRounded(asset, overpayment, loanScale),
|
||||
"xrpl::detail::computeOverpaymentComponents : valid overpayment "
|
||||
"amount");
|
||||
}
|
||||
|
||||
// First, deduct the fixed overpayment fee from the total amount.
|
||||
// This reduces the effective payment that will be applied to the loan.
|
||||
@@ -2055,6 +2088,7 @@ loanMakePayment(
|
||||
{
|
||||
detail::ExtendedPaymentComponents const overpaymentComponents =
|
||||
detail::computeOverpaymentComponents(
|
||||
view.rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
overpayment,
|
||||
|
||||
@@ -24,6 +24,13 @@ DatabaseNodeImp::store(NodeObjectType type, Blob&& data, uint256 const& hash, st
|
||||
|
||||
auto obj = NodeObject::createObject(type, std::move(data), hash);
|
||||
backend_->store(obj);
|
||||
if (cache_)
|
||||
{
|
||||
// After the store, replace a negative cache entry if there is one
|
||||
cache_->canonicalize(hash, obj, [](std::shared_ptr<NodeObject> const& n) {
|
||||
return n->getType() == NodeObjectType::Dummy;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -32,9 +39,25 @@ DatabaseNodeImp::asyncFetch(
|
||||
std::uint32_t ledgerSeq,
|
||||
std::function<void(std::shared_ptr<NodeObject> const&)>&& callback)
|
||||
{
|
||||
if (cache_)
|
||||
{
|
||||
std::shared_ptr<NodeObject> const obj = cache_->fetch(hash);
|
||||
if (obj)
|
||||
{
|
||||
callback(obj->getType() == NodeObjectType::Dummy ? nullptr : obj);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Database::asyncFetch(hash, ledgerSeq, std::move(callback));
|
||||
}
|
||||
|
||||
void
|
||||
DatabaseNodeImp::sweep()
|
||||
{
|
||||
if (cache_)
|
||||
cache_->sweep();
|
||||
}
|
||||
|
||||
std::shared_ptr<NodeObject>
|
||||
DatabaseNodeImp::fetchNodeObject(
|
||||
uint256 const& hash,
|
||||
@@ -42,32 +65,58 @@ DatabaseNodeImp::fetchNodeObject(
|
||||
FetchReport& fetchReport,
|
||||
bool duplicate)
|
||||
{
|
||||
std::shared_ptr<NodeObject> nodeObject = nullptr;
|
||||
Status status = Status::Ok;
|
||||
std::shared_ptr<NodeObject> nodeObject = cache_ ? cache_->fetch(hash) : nullptr;
|
||||
if (!nodeObject)
|
||||
{
|
||||
JLOG(j_.trace()) << "fetchNodeObject " << hash << ": record not "
|
||||
<< (cache_ ? "cached" : "found");
|
||||
|
||||
try
|
||||
{
|
||||
status = backend_->fetch(hash, &nodeObject);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j_.fatal()) << "fetchNodeObject " << hash
|
||||
<< ": Exception fetching from backend: " << e.what();
|
||||
rethrow();
|
||||
}
|
||||
Status status = Status::Ok;
|
||||
try
|
||||
{
|
||||
status = backend_->fetch(hash, &nodeObject);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j_.fatal()) << "fetchNodeObject " << hash
|
||||
<< ": Exception fetching from backend: " << e.what();
|
||||
rethrow();
|
||||
}
|
||||
|
||||
switch (status)
|
||||
switch (status)
|
||||
{
|
||||
case Status::Ok:
|
||||
if (cache_)
|
||||
{
|
||||
if (nodeObject)
|
||||
{
|
||||
cache_->canonicalizeReplaceClient(hash, nodeObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto notFound = NodeObject::createObject(NodeObjectType::Dummy, {}, hash);
|
||||
cache_->canonicalizeReplaceClient(hash, notFound);
|
||||
if (notFound->getType() != NodeObjectType::Dummy)
|
||||
nodeObject = notFound;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Status::NotFound:
|
||||
break;
|
||||
case Status::DataCorrupt:
|
||||
JLOG(j_.fatal()) << "fetchNodeObject " << hash << ": nodestore data is corrupted";
|
||||
break;
|
||||
default:
|
||||
JLOG(j_.warn()) << "fetchNodeObject " << hash << ": backend returns unknown result "
|
||||
<< static_cast<int>(status);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
case Status::Ok:
|
||||
case Status::NotFound:
|
||||
break;
|
||||
case Status::DataCorrupt:
|
||||
JLOG(j_.fatal()) << "fetchNodeObject " << hash << ": nodestore data is corrupted";
|
||||
break;
|
||||
default:
|
||||
JLOG(j_.warn()) << "fetchNodeObject " << hash << ": backend returns unknown result "
|
||||
<< static_cast<int>(status);
|
||||
break;
|
||||
JLOG(j_.trace()) << "fetchNodeObject " << hash << ": record found in cache";
|
||||
if (nodeObject->getType() == NodeObjectType::Dummy)
|
||||
nodeObject.reset();
|
||||
}
|
||||
|
||||
if (nodeObject)
|
||||
|
||||
@@ -113,6 +113,12 @@ DatabaseRotatingImp::store(NodeObjectType type, Blob&& data, uint256 const& hash
|
||||
storeStats(1, nObj->getData().size());
|
||||
}
|
||||
|
||||
void
|
||||
DatabaseRotatingImp::sweep()
|
||||
{
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
std::shared_ptr<NodeObject>
|
||||
DatabaseRotatingImp::fetchNodeObject(
|
||||
uint256 const& hash,
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace {
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
char const* const versionString = "3.2.0-rc2"
|
||||
char const* const versionString = "3.2.0-rc3"
|
||||
// clang-format on
|
||||
;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -513,7 +514,9 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
auto const expectedOverpaymentManagementFee = Number{10}; // 10% of 100
|
||||
auto const expectedPrincipalPortion = Number{400}; // 1,000 - 100 - 500
|
||||
|
||||
Env const env{*this};
|
||||
auto const components = xrpl::detail::computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
iou,
|
||||
loanScale,
|
||||
overpayment,
|
||||
@@ -854,7 +857,13 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
Number const overpaymentAmount{50};
|
||||
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
asset, loanScale, overpaymentAmount, TenthBips32(0), TenthBips32(0), managementFeeRate);
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
overpaymentAmount,
|
||||
TenthBips32(0),
|
||||
TenthBips32(0),
|
||||
managementFeeRate);
|
||||
|
||||
auto const loanProperties = computeLoanProperties(
|
||||
env.current()->rules(),
|
||||
@@ -942,6 +951,7 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
|
||||
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
Number{50, 0},
|
||||
@@ -1037,6 +1047,7 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
|
||||
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
Number{50, 0},
|
||||
@@ -1138,6 +1149,7 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
|
||||
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
Number{50, 0},
|
||||
@@ -1247,6 +1259,7 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
|
||||
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
Number{50, 0},
|
||||
@@ -1344,7 +1357,6 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
using namespace jtx;
|
||||
using namespace xrpl::detail;
|
||||
|
||||
Env const env{*this};
|
||||
Account const issuer{"issuer"};
|
||||
PrettyAsset const asset = issuer["USD"];
|
||||
std::int32_t const loanScale = -5;
|
||||
@@ -1355,7 +1367,9 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
std::uint32_t const paymentsRemaining = 10;
|
||||
auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
|
||||
|
||||
Env const env{*this};
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
Number{50, 0},
|
||||
@@ -1363,87 +1377,97 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
TenthBips32(10'000), // 10% overpayment fee
|
||||
managementFeeRate);
|
||||
|
||||
auto const loanProperties = computeLoanProperties(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanPrincipal,
|
||||
loanInterestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining,
|
||||
managementFeeRate,
|
||||
loanScale);
|
||||
struct Outcome
|
||||
{
|
||||
LoanPaymentParts parts;
|
||||
LoanState oldState;
|
||||
LoanState newState;
|
||||
};
|
||||
|
||||
auto const ret = tryOverpayment(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
overpaymentComponents,
|
||||
loanProperties.loanState,
|
||||
loanProperties.periodicPayment,
|
||||
periodicRate,
|
||||
paymentsRemaining,
|
||||
managementFeeRate,
|
||||
env.journal);
|
||||
// Run tryOverpayment under a given amendment set. At this (non-near-zero)
|
||||
// rate computeLoanProperties is amendment-independent, so the loan state
|
||||
// is identical across the amendment; only tryOverpayment's fixCleanup3_2_0
|
||||
// behaviour (the exact-principal pin and the management-fee re-derivation
|
||||
// from that principal) differs.
|
||||
auto run = [&](FeatureBitset features) -> std::optional<Outcome> {
|
||||
Env const env{*this, features};
|
||||
auto const loanProperties = computeLoanProperties(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanPrincipal,
|
||||
loanInterestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining,
|
||||
managementFeeRate,
|
||||
loanScale);
|
||||
auto const ret = tryOverpayment(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
overpaymentComponents,
|
||||
loanProperties.loanState,
|
||||
loanProperties.periodicPayment,
|
||||
periodicRate,
|
||||
paymentsRemaining,
|
||||
managementFeeRate,
|
||||
env.journal);
|
||||
if (!BEAST_EXPECT(ret))
|
||||
return std::nullopt;
|
||||
return Outcome{
|
||||
.parts = ret->first,
|
||||
.oldState = loanProperties.loanState,
|
||||
.newState = ret->second.loanState};
|
||||
};
|
||||
|
||||
BEAST_EXPECT(ret);
|
||||
auto const fixedOpt = run(testableAmendments());
|
||||
auto const legacyOpt = run(testableAmendments() - fixCleanup3_2_0);
|
||||
if (!fixedOpt || !legacyOpt)
|
||||
{
|
||||
BEAST_EXPECT(fixedOpt.has_value());
|
||||
BEAST_EXPECT(legacyOpt.has_value());
|
||||
return;
|
||||
}
|
||||
Outcome const& fixed = *fixedOpt;
|
||||
Outcome const& legacy = *legacyOpt;
|
||||
|
||||
auto const& [actualPaymentParts, newLoanProperties] = *ret;
|
||||
auto const& newState = newLoanProperties.loanState;
|
||||
// Components that the amendment does not change. The management fee is
|
||||
// charged against the overpayment interest portion first, so interest
|
||||
// paid stays 4.5 and fee paid 5.5; the principal repaid is 40 in both.
|
||||
auto checkCommon = [&](Outcome const& o, char const* tag) {
|
||||
BEAST_EXPECTS(
|
||||
(o.parts.interestPaid == Number{45, -1}),
|
||||
std::string(tag) + " interestPaid " + to_string(o.parts.interestPaid));
|
||||
BEAST_EXPECTS(
|
||||
(o.parts.feePaid == Number{55, -1}),
|
||||
std::string(tag) + " feePaid " + to_string(o.parts.feePaid));
|
||||
BEAST_EXPECTS(
|
||||
o.parts.principalPaid == 40,
|
||||
std::string(tag) + " principalPaid " + to_string(o.parts.principalPaid));
|
||||
BEAST_EXPECT(
|
||||
o.parts.principalPaid ==
|
||||
o.oldState.principalOutstanding - o.newState.principalOutstanding);
|
||||
// v = p + i + m identity: the non-interest part of valueChange equals
|
||||
// the interest-due change.
|
||||
BEAST_EXPECT(
|
||||
o.parts.valueChange - o.parts.interestPaid ==
|
||||
o.newState.interestDue - o.oldState.interestDue);
|
||||
};
|
||||
checkCommon(fixed, "fixed");
|
||||
checkCommon(legacy, "legacy");
|
||||
|
||||
// =========== VALIDATE PAYMENT PARTS ===========
|
||||
|
||||
// Since there is loan management fee, the fee is charged against
|
||||
// overpayment interest portion first, so interest paid remains 4.5
|
||||
BEAST_EXPECTS(
|
||||
(actualPaymentParts.interestPaid == Number{45, -1}),
|
||||
" interestPaid mismatch: expected 4.5, got " +
|
||||
to_string(actualPaymentParts.interestPaid));
|
||||
|
||||
// With overpayment interest portion, value change should equal the
|
||||
// interest decrease plus overpayment interest portion
|
||||
BEAST_EXPECTS(
|
||||
(actualPaymentParts.valueChange ==
|
||||
Number{-164737, -5} + actualPaymentParts.interestPaid),
|
||||
" valueChange mismatch: expected " +
|
||||
to_string(Number{-164737, -5} + actualPaymentParts.interestPaid) + ", got " +
|
||||
to_string(actualPaymentParts.valueChange));
|
||||
|
||||
// While there is no overpayment fee, fee paid should equal the
|
||||
// management fee charged against the overpayment interest portion
|
||||
BEAST_EXPECTS(
|
||||
(actualPaymentParts.feePaid == Number{55, -1}),
|
||||
" feePaid mismatch: expected 5.5, got " + to_string(actualPaymentParts.feePaid));
|
||||
|
||||
BEAST_EXPECTS(
|
||||
actualPaymentParts.principalPaid == 40,
|
||||
" principalPaid mismatch: expected 40, got `" +
|
||||
to_string(actualPaymentParts.principalPaid));
|
||||
|
||||
// =========== VALIDATE STATE CHANGES ===========
|
||||
|
||||
BEAST_EXPECTS(
|
||||
actualPaymentParts.principalPaid ==
|
||||
loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
|
||||
" principalPaid mismatch: expected " +
|
||||
to_string(
|
||||
loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
|
||||
", got " + to_string(actualPaymentParts.principalPaid));
|
||||
|
||||
// Note that the management fee value change is not captured, as this
|
||||
// value is not needed to correctly update the Vault state.
|
||||
BEAST_EXPECTS(
|
||||
(newState.managementFeeDue - loanProperties.loanState.managementFeeDue ==
|
||||
Number{-18304, -5}),
|
||||
" management fee change mismatch: expected " + to_string(Number{-18304, -5}) +
|
||||
", got " +
|
||||
to_string(newState.managementFeeDue - loanProperties.loanState.managementFeeDue));
|
||||
|
||||
BEAST_EXPECTS(
|
||||
actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
|
||||
newState.interestDue - loanProperties.loanState.interestDue,
|
||||
" valueChange mismatch: expected " +
|
||||
to_string(newState.interestDue - loanProperties.loanState.interestDue) + ", got " +
|
||||
to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid));
|
||||
// With fixCleanup3_2_0 the management fee is re-derived from the exact
|
||||
// principal; without it, from the one-scale-unit-high round-trip
|
||||
// principal. So the management fee outstanding (and hence the value
|
||||
// change, via v = p + i + m) differ by exactly one scale-unit (1e-5 at
|
||||
// loanScale -5) between the two paths.
|
||||
BEAST_EXPECT((fixed.parts.valueChange == Number{-164738, -5} + fixed.parts.interestPaid));
|
||||
BEAST_EXPECT(
|
||||
(fixed.newState.managementFeeDue - fixed.oldState.managementFeeDue ==
|
||||
Number{-18303, -5}));
|
||||
BEAST_EXPECT((legacy.parts.valueChange == Number{-164737, -5} + legacy.parts.interestPaid));
|
||||
BEAST_EXPECT(
|
||||
(legacy.newState.managementFeeDue - legacy.oldState.managementFeeDue ==
|
||||
Number{-18304, -5}));
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
@@ -7580,6 +7580,366 @@ protected:
|
||||
attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
|
||||
}
|
||||
|
||||
// A residual overpayment can reduce the stored principal by one scale-unit
|
||||
// *less* than computeOverpaymentComponents predicts, firing the
|
||||
// "principal change agrees" XRPL_ASSERT_PARTS in doOverpayment:
|
||||
//
|
||||
// trackedPrincipalDelta == principalOutstanding - newPrincipalOutstanding
|
||||
//
|
||||
// tryOverpayment re-amortizes the loan at the reduced principal, then
|
||||
// re-derives the theoretical principal from the new periodic payment via
|
||||
// (P * paymentFactor) / paymentFactor. That round-trip is not exact in
|
||||
// Number's 19-digit arithmetic; a positive residual pushes the recomputed
|
||||
// principal a hair above the exact grid point `oldPrincipal - delta`, and
|
||||
// the Upward rounding in tryOverpayment then bumps it a full scale-unit
|
||||
// higher. The principal therefore drops by `delta - 1 unit`, not `delta`.
|
||||
//
|
||||
// Concrete case (isolated, at the tryOverpayment level):
|
||||
// A 100 USD loan at the minimum non-zero rate, 3 payments, loanScale -10.
|
||||
// After one regular payment (principalOutstanding 66.6666666674) a residual overpayment of
|
||||
// 0.049999998 yields trackedPrincipalDelta 0.048999998 but only reduces the principal by
|
||||
// 0.0489999979 (newPrincipal 66.6176666695) — short by 1e-10.
|
||||
//
|
||||
// With fixCleanup3_2_0, tryOverpayment pins the new principal to the exact,
|
||||
// on-grid reduction (oldPrincipal - trackedPrincipalDelta) instead of the
|
||||
// lossy (P*factor)/factor round-trip, so the assertion holds and the
|
||||
// overpayment applies cleanly. The three "principal change agrees" /
|
||||
// "interest paid agrees" / "principal payment matches" assertions are
|
||||
// gated behind the same amendment, so without it they are disabled (the
|
||||
// server does not abort) and the loan keeps the pre-amendment computation.
|
||||
//
|
||||
// The test runs the same scenario under both amendment settings and checks
|
||||
// the stored principal against a ground-truth value derived independently of
|
||||
// the loan-state computation under test.
|
||||
void
|
||||
testBugOverpaymentPrincipalChange()
|
||||
{
|
||||
testcase("bug: doOverpayment asserts 'principal change agrees'");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace loan;
|
||||
using namespace xrpl::detail;
|
||||
|
||||
struct Params
|
||||
{
|
||||
TenthBips32 interestRate;
|
||||
TenthBips16 managementFeeRate;
|
||||
std::uint32_t paymentTotal;
|
||||
std::uint32_t paymentInterval;
|
||||
std::int64_t principal;
|
||||
Number overpayment;
|
||||
TenthBips32 overpaymentInterestRate;
|
||||
TenthBips32 overpaymentFeeRate;
|
||||
std::optional<int> vaultScale;
|
||||
};
|
||||
|
||||
struct Result
|
||||
{
|
||||
Number principalOutstanding; // stored principal after the LoanPay
|
||||
Number expectedNewPrincipal; // ground truth, independent of the fix
|
||||
Number managementFeeChange; // managementFeeOutstanding after - before
|
||||
Number unit; // one scale-unit at the loan scale
|
||||
};
|
||||
|
||||
auto runScenario = [this](FeatureBitset features, Params const& p) -> Result {
|
||||
Env env(*this, features);
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const lender{"vaultOwner"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
env.fund(XRP(1'000'000), issuer, lender, borrower);
|
||||
env(fset(issuer, asfDefaultRipple));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const iouAsset = issuer["USD"];
|
||||
Asset const asset = iouAsset.raw();
|
||||
STAmount const iouLimit{asset, Number{9'999'999'999'999'999LL}};
|
||||
env(trust(lender, iouLimit));
|
||||
env(trust(borrower, iouLimit));
|
||||
env(pay(issuer, lender, iouAsset(1'000'000)));
|
||||
env(pay(issuer, borrower, iouAsset(1'000'000)));
|
||||
env.close();
|
||||
|
||||
auto const broker = createVaultAndBroker(
|
||||
env,
|
||||
iouAsset,
|
||||
lender,
|
||||
{.vaultDeposit = 900'000,
|
||||
.debtMax = 0,
|
||||
.managementFeeRate = p.managementFeeRate,
|
||||
.vaultScale = p.vaultScale});
|
||||
|
||||
auto const brokerSle = env.le(broker.brokerKeylet());
|
||||
BEAST_EXPECT(brokerSle);
|
||||
auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
|
||||
auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
|
||||
|
||||
env(set(borrower, broker.brokerID, Number{p.principal}, tfLoanOverpayment),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
kInterestRate(p.interestRate),
|
||||
kPaymentTotal(p.paymentTotal),
|
||||
kPaymentInterval(p.paymentInterval),
|
||||
kGracePeriod(p.paymentInterval),
|
||||
kOverpaymentFee(p.overpaymentFeeRate),
|
||||
kOverpaymentInterestRate(p.overpaymentInterestRate),
|
||||
Fee(env.current()->fees().base * 2),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// The single LoanPay below makes one regular payment (the overpayment
|
||||
// is smaller than one period) and leaves the residual as an
|
||||
// overpayment.
|
||||
auto const s = getCurrentState(env, broker, loanKeylet);
|
||||
auto const periodicRate = loanPeriodicRate(s.interestRate, s.paymentInterval);
|
||||
auto const onePeriod = computePaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
s.loanScale,
|
||||
s.totalValue,
|
||||
s.principalOutstanding,
|
||||
s.managementFeeOutstanding,
|
||||
s.periodicPayment,
|
||||
periodicRate,
|
||||
s.paymentRemaining,
|
||||
p.managementFeeRate);
|
||||
|
||||
// Ground truth: the stored principal must drop by exactly the regular
|
||||
// payment's principal portion plus the overpayment's principal
|
||||
// portion. computeOverpaymentComponents depends only on the
|
||||
// overpayment amount and rates (not on the loan-state computation
|
||||
// under test), so it is an independent oracle. Both components are
|
||||
// computed under the same rules as the env so the payment factor
|
||||
// matches.
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
s.loanScale,
|
||||
p.overpayment,
|
||||
p.overpaymentInterestRate,
|
||||
p.overpaymentFeeRate,
|
||||
p.managementFeeRate);
|
||||
Number const expectedNewPrincipal = s.principalOutstanding -
|
||||
onePeriod.trackedPrincipalDelta - overpaymentComponents.trackedPrincipalDelta;
|
||||
|
||||
Number const managementFeeBefore = s.managementFeeOutstanding;
|
||||
|
||||
STAmount const payAmount{asset, onePeriod.trackedValueDelta + p.overpayment};
|
||||
env(pay(borrower, loanKeylet.key, payAmount),
|
||||
Txflags(tfLoanOverpayment),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const loanSle = env.le(loanKeylet);
|
||||
BEAST_EXPECT(loanSle);
|
||||
|
||||
return Result{
|
||||
.principalOutstanding = loanSle ? Number{loanSle->at(sfPrincipalOutstanding)} : 0,
|
||||
.expectedNewPrincipal = expectedNewPrincipal,
|
||||
.managementFeeChange =
|
||||
(loanSle ? Number{loanSle->at(sfManagementFeeOutstanding)} : Number{0}) -
|
||||
managementFeeBefore,
|
||||
.unit = Number{1, s.loanScale}};
|
||||
};
|
||||
|
||||
// Scenario 1: the original near-zero-rate principal reproduction
|
||||
// (loanScale -10, no management fee). 0.049999998 is smaller than one
|
||||
// period, so it stays a residual overpayment.
|
||||
Params const principalCase{
|
||||
.interestRate = TenthBips32{1},
|
||||
.managementFeeRate = TenthBips16{0},
|
||||
.paymentTotal = 3,
|
||||
.paymentInterval = 60,
|
||||
.principal = 100,
|
||||
.overpayment = Number{49999998, -9},
|
||||
.overpaymentInterestRate = TenthBips32{1000},
|
||||
.overpaymentFeeRate = TenthBips32{1000},
|
||||
.vaultScale = 1};
|
||||
|
||||
// With fixCleanup3_2_0 the stored principal lands exactly on the
|
||||
// ground-truth grid point: it is reduced by exactly the overpayment's
|
||||
// principal portion. This is the key correctness check: if the principal
|
||||
// pin were removed (even with the assertions still gated off), the lossy
|
||||
// (P * factor) / factor round-trip would leave the principal one
|
||||
// scale-unit high and this would fail.
|
||||
Result const fixed = runScenario(all_, principalCase);
|
||||
BEAST_EXPECTS(
|
||||
fixed.principalOutstanding == fixed.expectedNewPrincipal,
|
||||
"fixed principal " + to_string(fixed.principalOutstanding) + " != expected " +
|
||||
to_string(fixed.expectedNewPrincipal));
|
||||
|
||||
// Without the amendment the loan amortizes with the catastrophically
|
||||
// cancelling near-zero payment factor, so its schedule (and ground truth)
|
||||
// differ from the fixed case; the gated assertions keep the server from
|
||||
// aborting and the overpayment still lands exactly on that schedule.
|
||||
Result const legacy = runScenario(all_ - fixCleanup3_2_0, principalCase);
|
||||
BEAST_EXPECTS(
|
||||
legacy.principalOutstanding == legacy.expectedNewPrincipal,
|
||||
"legacy principal " + to_string(legacy.principalOutstanding) + " != expected " +
|
||||
to_string(legacy.expectedNewPrincipal));
|
||||
|
||||
// Scenario 2: a normal-rate loan with a 10% management fee. At a normal
|
||||
// rate the payment factor is identical across the amendment, so toggling
|
||||
// fixCleanup3_2_0 isolates the fix. This overpayment (found by search)
|
||||
// lands on a state where both the principal and the management fee differ
|
||||
// by one scale-unit between the fixed and legacy paths.
|
||||
Params const feeCase{
|
||||
.interestRate = TenthBips32{10000},
|
||||
.managementFeeRate = TenthBips16{10000},
|
||||
.paymentTotal = 6,
|
||||
.paymentInterval = 30u * 24 * 60 * 60,
|
||||
.principal = 1000,
|
||||
.overpayment = Number{214367363, -10},
|
||||
.overpaymentInterestRate = TenthBips32{0},
|
||||
.overpaymentFeeRate = TenthBips32{0},
|
||||
.vaultScale = std::nullopt};
|
||||
|
||||
Result const feeFixed = runScenario(all_, feeCase);
|
||||
Result const feeLegacy = runScenario(all_ - fixCleanup3_2_0, feeCase);
|
||||
|
||||
// With the fix the principal is the exact reduction; without it the lossy
|
||||
// (P * factor) / factor round-trip leaves it one scale-unit high.
|
||||
BEAST_EXPECTS(
|
||||
feeFixed.principalOutstanding == feeFixed.expectedNewPrincipal,
|
||||
"fee-case fixed principal " + to_string(feeFixed.principalOutstanding) +
|
||||
" != expected " + to_string(feeFixed.expectedNewPrincipal));
|
||||
BEAST_EXPECTS(
|
||||
feeLegacy.principalOutstanding == feeLegacy.expectedNewPrincipal + feeLegacy.unit,
|
||||
"fee-case legacy principal " + to_string(feeLegacy.principalOutstanding) +
|
||||
" != expected " + to_string(feeLegacy.expectedNewPrincipal + feeLegacy.unit));
|
||||
|
||||
// Management fee: the overpayment re-amortizes a fee-bearing loan, so the management fee
|
||||
// outstanding drops.
|
||||
//
|
||||
// Unlike the principal that is already at the correct precision, the re-amortized
|
||||
// management fee is tenthBipsOfValue of the new schedule's gross interest, which depends
|
||||
// on the recomputed periodic payment. So the expected change below is a pinned constant
|
||||
// captured from a passing run a magic value only because there is nothing simpler to
|
||||
// compare against.
|
||||
//
|
||||
// At the integration level, toggling the amendment also changes the regular payment's
|
||||
// rounding so a fixed-vs-legacy comparison cannot isolate the overpayment management-fee
|
||||
// fix.
|
||||
BEAST_EXPECT(feeFixed.managementFeeChange == feeLegacy.managementFeeChange);
|
||||
BEAST_EXPECTS(
|
||||
(feeFixed.managementFeeChange == Number{-8219709543, -10}),
|
||||
"fee-case mgmt fee change " + to_string(feeFixed.managementFeeChange));
|
||||
}
|
||||
|
||||
// A LoanSet with InterestRate = 1 (0.001% annualized, the minimum non-zero
|
||||
// rate). At such a near-zero rate the closed-form payment factor
|
||||
// (1 + r)^n - 1 cancels catastrophically.
|
||||
//
|
||||
// Without fixCleanup3_2_0 the resulting amortization is degenerate and the
|
||||
// LoanSet is rejected with tecPRECISION_LOSS (no loan created). With the
|
||||
// amendment, computePowerMinusOneHybrid uses a numerically-stable series
|
||||
// expansion, so the loan is created and the scheduled payments
|
||||
// (2 * periodicPayment) cover the principal — no economic underpayment
|
||||
// (yield theft).
|
||||
//
|
||||
// The test runs the same LoanSet under both amendment settings and pins the
|
||||
// exact outcome for each.
|
||||
void
|
||||
testLoanSetNearZeroInterestRateSucceeds()
|
||||
{
|
||||
testcase("LoanSet near-zero interest rate covers principal");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace loan;
|
||||
|
||||
Number const principalRequested{1000};
|
||||
|
||||
struct Result
|
||||
{
|
||||
TER ter = tesSUCCESS;
|
||||
bool created = false;
|
||||
std::int32_t loanScale = 0;
|
||||
Number principal;
|
||||
Number totalValue;
|
||||
Number managementFee;
|
||||
Number periodicPayment;
|
||||
};
|
||||
|
||||
auto runScenario = [&](FeatureBitset features, TER expectedTer) -> Result {
|
||||
Env env(*this, features);
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const lender{"vaultOwner"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
env.fund(XRP(1'000'000), issuer, lender, borrower);
|
||||
env(fset(issuer, asfDefaultRipple));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const iouAsset = issuer["USD"];
|
||||
STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}};
|
||||
env(trust(lender, iouLimit));
|
||||
env(trust(borrower, iouLimit));
|
||||
env(pay(issuer, lender, iouAsset(1'000'000)));
|
||||
env(pay(issuer, borrower, iouAsset(1'000'000)));
|
||||
env.close();
|
||||
|
||||
auto const broker = createVaultAndBroker(
|
||||
env,
|
||||
iouAsset,
|
||||
lender,
|
||||
{.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate = TenthBips16{0}});
|
||||
|
||||
auto const brokerSle = env.le(broker.brokerKeylet());
|
||||
BEAST_EXPECT(brokerSle);
|
||||
auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
|
||||
auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
|
||||
|
||||
env(set(borrower, broker.brokerID, principalRequested),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
kInterestRate(TenthBips32{1}),
|
||||
kPaymentTotal(2),
|
||||
kPaymentInterval(400),
|
||||
Fee(env.current()->fees().base * 2),
|
||||
Ter(expectedTer));
|
||||
env.close();
|
||||
|
||||
Result r;
|
||||
r.ter = env.ter();
|
||||
if (auto const loanSle = env.le(loanKeylet))
|
||||
{
|
||||
r.created = true;
|
||||
r.loanScale = loanSle->at(sfLoanScale);
|
||||
r.principal = loanSle->at(sfPrincipalOutstanding);
|
||||
r.totalValue = loanSle->at(sfTotalValueOutstanding);
|
||||
r.managementFee = loanSle->at(sfManagementFeeOutstanding);
|
||||
r.periodicPayment = loanSle->at(sfPeriodicPayment);
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
Result const fixed = runScenario(all_, tesSUCCESS);
|
||||
Result const legacy = runScenario(all_ - fixCleanup3_2_0, tecPRECISION_LOSS);
|
||||
|
||||
// Without the amendment, the catastrophically-cancelling closed-form
|
||||
// payment factor produces a degenerate amortization that fails
|
||||
// checkLoanGuards: the LoanSet is rejected with tecPRECISION_LOSS and no
|
||||
// loan is created.
|
||||
BEAST_EXPECT(legacy.ter == tecPRECISION_LOSS);
|
||||
BEAST_EXPECT(!legacy.created);
|
||||
|
||||
// With the amendment the stable series expansion produces a valid loan
|
||||
// at loanScale -10.
|
||||
BEAST_EXPECT(fixed.ter == tesSUCCESS);
|
||||
BEAST_EXPECT(fixed.created);
|
||||
BEAST_EXPECT(fixed.loanScale == -10);
|
||||
BEAST_EXPECT(fixed.principal == principalRequested);
|
||||
BEAST_EXPECT((fixed.totalValue == Number{10000000001903, -10}));
|
||||
BEAST_EXPECT(fixed.managementFee == beast::kZero);
|
||||
|
||||
// Periodic payment from the numerically-stable series expansion, and the
|
||||
// scheduled total (2 * periodicPayment) which exceeds the 1000 principal
|
||||
// — no economic underpayment / yield theft.
|
||||
BEAST_EXPECT((fixed.periodicPayment == Number{5000000000951293762, -16}));
|
||||
BEAST_EXPECT((fixed.periodicPayment * 2 == Number{1000000000190258752, -15}));
|
||||
BEAST_EXPECT(fixed.periodicPayment * 2 > principalRequested);
|
||||
}
|
||||
|
||||
// An overpayment whose residual amount has more precision than loanScale
|
||||
// fires the isRounded(asset, overpayment, loanScale) assertion in
|
||||
// computeOverpaymentComponents (and a downstream "interest paid agrees"
|
||||
@@ -8358,12 +8718,14 @@ protected:
|
||||
testLimitExceeded();
|
||||
testLoanSetBlockedLoanPayAllowedWhenCanTransferCleared();
|
||||
testLendingCanTradeClearedNoImpact();
|
||||
testBugOverpaymentPrincipalChange();
|
||||
testBugOverpayUnroundedAmount();
|
||||
|
||||
for (auto const flags : {0u, tfLoanOverpayment})
|
||||
testYieldTheftRounding(flags);
|
||||
testBugInterestDueDeltaCrash();
|
||||
testFullLifecycleVaultPnLNearZeroRate();
|
||||
testLoanSetNearZeroInterestRateSucceeds();
|
||||
}
|
||||
|
||||
// Tests run under each entry in amendmentCombinations().
|
||||
|
||||
@@ -520,6 +520,25 @@ public:
|
||||
/////////////////////////////////////////////////////////////
|
||||
// Create NodeStore with two backends to allow online deletion of data.
|
||||
// Normally, SHAMapStoreImp handles all these details.
|
||||
auto nscfg = env.app().config().section(ConfigSection::nodeDatabase());
|
||||
|
||||
// Provide default values.
|
||||
if (!nscfg.exists("cache_size"))
|
||||
{
|
||||
nscfg.set(
|
||||
"cache_size",
|
||||
std::to_string(
|
||||
env.app().config().getValueFor(SizedItem::TreeCacheSize, std::nullopt)));
|
||||
}
|
||||
|
||||
if (!nscfg.exists("cache_age"))
|
||||
{
|
||||
nscfg.set(
|
||||
"cache_age",
|
||||
std::to_string(
|
||||
env.app().config().getValueFor(SizedItem::TreeCacheAge, std::nullopt)));
|
||||
}
|
||||
|
||||
NodeStoreScheduler scheduler(env.app().getJobQueue());
|
||||
|
||||
std::string const writableDb = "write";
|
||||
@@ -528,7 +547,6 @@ public:
|
||||
auto archiveBackend = makeBackendRotating(env, scheduler, archiveDb);
|
||||
|
||||
static constexpr int kReadThreads = 4;
|
||||
auto nscfg = env.app().config().section(ConfigSection::nodeDatabase());
|
||||
auto dbr = std::make_unique<NodeStore::DatabaseRotatingImp>(
|
||||
scheduler,
|
||||
kReadThreads,
|
||||
|
||||
@@ -6,17 +6,23 @@
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
// NOLINTNEXTLINE(misc-include-cleaner)
|
||||
#include <boost/multiprecision/cpp_dec_float.hpp>
|
||||
#include <boost/multiprecision/number.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -40,6 +46,40 @@ class Number_test : public beast::unit_test::Suite
|
||||
return out;
|
||||
}
|
||||
|
||||
using dec = boost::multiprecision::cpp_dec_float_50;
|
||||
|
||||
template <class T = dec>
|
||||
static T
|
||||
pow10(int n)
|
||||
{
|
||||
if (n == 0)
|
||||
return 1;
|
||||
if (n == 1)
|
||||
return 10;
|
||||
|
||||
if (n > 1)
|
||||
{
|
||||
auto r = pow10<T>(n / 2);
|
||||
r *= r;
|
||||
if (n % 2 != 0)
|
||||
r *= 10;
|
||||
return r;
|
||||
}
|
||||
|
||||
// n < 0
|
||||
T p = 1;
|
||||
p /= pow10<T>(-n);
|
||||
return p;
|
||||
}
|
||||
|
||||
static std::string
|
||||
fmt(dec const& v)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os << std::setprecision(40) << v;
|
||||
return os.str();
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testZero()
|
||||
@@ -1349,10 +1389,103 @@ public:
|
||||
testRelationals()
|
||||
{
|
||||
testcase << "test_relationals " << to_string(Number::getMantissaScale());
|
||||
BEAST_EXPECT(!(Number{100} < Number{10}));
|
||||
BEAST_EXPECT(Number{100} > Number{10});
|
||||
BEAST_EXPECT(Number{100} >= Number{10});
|
||||
BEAST_EXPECT(!(Number{100} <= Number{10}));
|
||||
|
||||
{
|
||||
auto test = [this](auto const& nums) {
|
||||
BEAST_EXPECT(std::ranges::is_sorted(nums));
|
||||
|
||||
for (auto iter1 = nums.begin(); iter1 != nums.end(); ++iter1)
|
||||
{
|
||||
auto iter2 = iter1;
|
||||
for (++iter2; iter2 != nums.end(); ++iter2)
|
||||
{
|
||||
Number const& smaller = *iter1;
|
||||
Number const& larger = *iter2;
|
||||
std::stringstream ss;
|
||||
ss << smaller << " < " << larger;
|
||||
auto const str = ss.str();
|
||||
|
||||
// The ==/!= operators use a completely different code path than <, etc.
|
||||
// This helps detect a breakage in one but not the other. It also helps
|
||||
// verify that the values are being ordered correctly.
|
||||
BEAST_EXPECTS(smaller != larger, str + " (!=)");
|
||||
BEAST_EXPECTS(!(smaller == larger), str + " (==)");
|
||||
|
||||
// true results using operator< and derived operators
|
||||
BEAST_EXPECTS(smaller < larger, str + " (<)");
|
||||
BEAST_EXPECTS(larger > smaller, str + " (>)");
|
||||
BEAST_EXPECTS(larger >= smaller, str + " (>=)");
|
||||
BEAST_EXPECTS(smaller <= larger, str + " (<=)");
|
||||
|
||||
// false results using operator< and derived operators
|
||||
BEAST_EXPECTS(!(larger < smaller), str + " (! <)");
|
||||
BEAST_EXPECTS(!(smaller > larger), str + " (! >)");
|
||||
BEAST_EXPECTS(!(smaller >= larger), str + " (! >=)");
|
||||
BEAST_EXPECTS(!(larger <= smaller), str + " (! <=)");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto const intNums = [this]() {
|
||||
// Inequality test cases are built from a list of sorted integers
|
||||
auto const values =
|
||||
std::to_array<int>({-100, -50, -20, -10, -1, 0, 1, 10, 20, 50, 100});
|
||||
// Check this list is sorted before converting it to Numbers.
|
||||
// That way if any of the other tests fail, we know it's because of code and not the
|
||||
// source data.
|
||||
BEAST_EXPECT(std::ranges::is_sorted(values));
|
||||
|
||||
std::vector<Number> result;
|
||||
result.reserve(values.size());
|
||||
for (auto const v : values)
|
||||
result.emplace_back(v);
|
||||
return result;
|
||||
}();
|
||||
|
||||
auto const otherNums = std::to_array<Number>({
|
||||
Number{-5, 100},
|
||||
Number{-1, 100},
|
||||
Number{-7, -10},
|
||||
Number{-2, -10},
|
||||
Number{0},
|
||||
Number{2, -10},
|
||||
Number{7, -10},
|
||||
Number{1, 100},
|
||||
Number{5, 100},
|
||||
});
|
||||
|
||||
test(intNums);
|
||||
test(otherNums);
|
||||
}
|
||||
|
||||
{
|
||||
// Equality test cases are <Number, __LINE__>. Number will be compared against itself
|
||||
using Case = std::pair<Number, int>;
|
||||
auto const c = std::to_array<Case>({
|
||||
{700, __LINE__},
|
||||
{50, __LINE__},
|
||||
{1, __LINE__},
|
||||
{0, __LINE__},
|
||||
{-1, __LINE__},
|
||||
{-30, __LINE__},
|
||||
{-600, __LINE__},
|
||||
});
|
||||
for (auto const& [n, line] : c)
|
||||
{
|
||||
auto const str = to_string(n);
|
||||
|
||||
// NOLINTBEGIN(misc-redundant-expression) Explicitly testing operators with
|
||||
// equivalent values
|
||||
expect(n == n, str + " ==", __FILE__, line);
|
||||
expect(!(n != n), str + " !=", __FILE__, line);
|
||||
|
||||
expect(!(n < n), str + " < ", __FILE__, line);
|
||||
expect(!(n > n), str + " >", __FILE__, line);
|
||||
expect(n >= n, str + " >=", __FILE__, line);
|
||||
expect(n <= n, str + " <=", __FILE__, line);
|
||||
// NOLINTEND(misc-redundant-expression)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1589,39 +1722,249 @@ public:
|
||||
void
|
||||
testUpwardRoundsDown()
|
||||
{
|
||||
testcase << "upward rounding produces a value below exact at kMaxRep cusp";
|
||||
auto const scale = Number::getMantissaScale();
|
||||
{
|
||||
testcase << "upward rounding produces a value below exact at kMaxRep cusp "
|
||||
<< to_string(scale);
|
||||
|
||||
NumberMantissaScaleGuard const mg{MantissaRange::MantissaScale::Large};
|
||||
NumberRoundModeGuard const rg{Number::RoundingMode::Upward};
|
||||
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;
|
||||
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;
|
||||
Number const a = kAValue;
|
||||
Number const b = kBValue;
|
||||
Number const product = a * b;
|
||||
|
||||
// Exact reference in BigInt.
|
||||
BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue);
|
||||
// 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;
|
||||
// What Number actually stored.
|
||||
BigInt storedValue = BigInt(product.mantissa());
|
||||
for (int i = 0; i < product.exponent(); ++i)
|
||||
storedValue *= 10;
|
||||
|
||||
BigInt const signedDifference = storedValue - exactProduct;
|
||||
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";
|
||||
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"
|
||||
<< " stored.mantissa = " << product.mantissa() << "\n"
|
||||
<< " stored.exponent = " << product.exponent() << "\n";
|
||||
log.flush();
|
||||
|
||||
BEAST_EXPECT(signedDifference >= 0);
|
||||
BEAST_EXPECT(product.mantissa() == (std::numeric_limits<std::int64_t>::max() / 10) + 1);
|
||||
BEAST_EXPECT(product.exponent() == 19);
|
||||
switch (scale)
|
||||
{
|
||||
case MantissaRange::MantissaScale::Large:
|
||||
BEAST_EXPECT(signedDifference >= 0);
|
||||
BEAST_EXPECT(signedDifference < pow10<BigInt>(product.exponent()));
|
||||
BEAST_EXPECT(
|
||||
product.mantissa() == (std::numeric_limits<std::int64_t>::max() / 10) + 1);
|
||||
BEAST_EXPECT(product.exponent() == 19);
|
||||
break;
|
||||
|
||||
case MantissaRange::MantissaScale::LargeLegacy:
|
||||
BEAST_EXPECT(signedDifference < 0);
|
||||
BEAST_EXPECT(
|
||||
product.mantissa() ==
|
||||
(std::numeric_limits<std::int64_t>::max() / 100) * 100);
|
||||
BEAST_EXPECT(product.exponent() == 18);
|
||||
break;
|
||||
|
||||
case MantissaRange::MantissaScale::Small:
|
||||
// The seemingly weird rounding here is because
|
||||
// a & b are both normalized, and both round up when
|
||||
// being converted to Number, so you're really
|
||||
// getting 1_000_000_000_000_050 * 9_223_372_036_854_316
|
||||
BEAST_EXPECT(signedDifference >= 0);
|
||||
BEAST_EXPECT(
|
||||
product.mantissa() ==
|
||||
(std::numeric_limits<std::int64_t>::max() / 1000) + 3);
|
||||
BEAST_EXPECT(product.exponent() == 21);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* Companion regression for the kMaxRep cusp behavior, but for
|
||||
* `operator/=` on the cusp-fix-ENABLED `Large` scale.
|
||||
*
|
||||
* Before the dropped-remainder fix, `operator/=` with Upward
|
||||
* rounding could return a value STRICTLY LESS than the exact quotient,
|
||||
* violating Upward's directional invariant.
|
||||
*
|
||||
* Mechanism (fix-enabled path):
|
||||
* 1. `operator/=` computes `numerator = nm * 10^17` and
|
||||
* `zm = numerator / dm` (integer division, truncates remainder).
|
||||
* 2. If `remainder != 0`, the correction block runs:
|
||||
* zm *= 100000
|
||||
* correction = (remainder * 100000) / dm // also truncates
|
||||
* zm += correction
|
||||
* ze -= 5
|
||||
* The truncation in `correction` discards a sub-1/100000 residual.
|
||||
* 3. `normalize`'s shift loop reduces zm to fit, but the discarded
|
||||
* residual is BELOW the Guard's visibility, so the Guard sees fraction = 0.
|
||||
* 4. Under Upward + positive, `round()` returns -1 (no round-up), and
|
||||
* the algorithm returns the truncated zm
|
||||
*/
|
||||
testcase << "operator/= Upward on Large returns value < truth " << to_string(scale);
|
||||
|
||||
NumberRoundModeGuard const roundGuard{Number::RoundingMode::Upward};
|
||||
|
||||
constexpr std::int64_t aValue = 2LL;
|
||||
constexpr std::int64_t bValue = 1'000'000'000'000'000'007LL;
|
||||
// bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]).
|
||||
|
||||
Number const a{aValue, 0};
|
||||
Number const b{bValue, 0};
|
||||
Number const quotient = a / b;
|
||||
|
||||
dec const exact = dec(aValue) / dec(bValue);
|
||||
dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent());
|
||||
dec const diff = stored - exact;
|
||||
|
||||
log << "\n"
|
||||
<< " a = " << aValue << "\n"
|
||||
<< " b = " << bValue << "\n"
|
||||
<< " exact a/b = " << fmt(exact) << "\n"
|
||||
<< " stored a/b = " << fmt(stored) << "\n"
|
||||
<< " stored - exact = " << fmt(diff)
|
||||
<< " (negative => Upward gave value BELOW truth)\n"
|
||||
<< " quotient.mantissa = " << quotient.mantissa() << "\n"
|
||||
<< " quotient.exponent = " << quotient.exponent() << "\n";
|
||||
log.flush();
|
||||
|
||||
// Upward invariant: stored >= exact. Bug: stored < exact.
|
||||
switch (scale)
|
||||
{
|
||||
case MantissaRange::MantissaScale::Large:
|
||||
BEAST_EXPECT(stored >= exact);
|
||||
BEAST_EXPECT(diff < pow10(quotient.exponent()));
|
||||
break;
|
||||
|
||||
case MantissaRange::MantissaScale::LargeLegacy:
|
||||
BEAST_EXPECT(stored < exact);
|
||||
BEAST_EXPECT(diff >= -pow10(quotient.exponent()));
|
||||
break;
|
||||
|
||||
case MantissaRange::MantissaScale::Small:
|
||||
// Small mantissa doesn't have the correction for
|
||||
// dropped remainders
|
||||
BEAST_EXPECT(stored < exact);
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
/* Companion test case for Upward positive operator/=: Downward negative
|
||||
*/
|
||||
testcase << "operator/= Downward on Large returns value < truth " << to_string(scale);
|
||||
|
||||
NumberRoundModeGuard const roundGuard{Number::RoundingMode::Downward};
|
||||
|
||||
constexpr std::int64_t aValue = -2LL;
|
||||
constexpr std::int64_t bValue = 1'000'000'000'000'000'007LL;
|
||||
// bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]).
|
||||
|
||||
Number const a{aValue, 0};
|
||||
Number const b{bValue, 0};
|
||||
Number const quotient = a / b;
|
||||
|
||||
dec const exact = dec(aValue) / dec(bValue);
|
||||
dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent());
|
||||
dec const diff = stored - exact;
|
||||
|
||||
log << "\n"
|
||||
<< " a = " << aValue << "\n"
|
||||
<< " b = " << bValue << "\n"
|
||||
<< " exact a/b = " << fmt(exact) << "\n"
|
||||
<< " stored a/b = " << fmt(stored) << "\n"
|
||||
<< " stored - exact = " << fmt(diff)
|
||||
<< " (positive => Downward gave value ABOVE truth)\n"
|
||||
<< " quotient.mantissa = " << quotient.mantissa() << "\n"
|
||||
<< " quotient.exponent = " << quotient.exponent() << "\n";
|
||||
log.flush();
|
||||
|
||||
// invariant: stored <= exact. Bug: stored > exact.
|
||||
switch (scale)
|
||||
{
|
||||
case MantissaRange::MantissaScale::Large:
|
||||
BEAST_EXPECT(stored <= exact);
|
||||
BEAST_EXPECT(diff > -pow10(quotient.exponent()));
|
||||
break;
|
||||
|
||||
case MantissaRange::MantissaScale::LargeLegacy:
|
||||
BEAST_EXPECT(stored > exact);
|
||||
BEAST_EXPECT(diff <= pow10(quotient.exponent()));
|
||||
break;
|
||||
|
||||
case MantissaRange::MantissaScale::Small:
|
||||
// Small mantissa doesn't have the correction for
|
||||
// dropped remainders
|
||||
BEAST_EXPECT(stored < exact);
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
/* Companion test case for Upward positive operator/=: ToNearest
|
||||
*
|
||||
* With ToNearest, if the dropped digits are exactly "5", then the mantissa will be
|
||||
* rounded to even. The numbers below result in a value where the unrounded mantissa
|
||||
* ends in an even digit, and "infinite precision" would drop
|
||||
* "500000000000000000145...", but doNormalize only sees "5". Without the rounding fix,
|
||||
* doNormalize rounds down to the even value. With the rounding fix, doNormalize knows
|
||||
* there are more digits beyond "5", and so rounds _up_ to the odd value.
|
||||
*/
|
||||
testcase << "operator/= ToNearest on Large returns value < truth " << to_string(scale);
|
||||
|
||||
NumberRoundModeGuard const roundGuard{Number::RoundingMode::ToNearest};
|
||||
|
||||
constexpr std::int64_t aValue = 1'269'917'268'816'087'809LL;
|
||||
constexpr std::int64_t bValue = 3'458'525'013'821'685'511LL;
|
||||
// bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]).
|
||||
|
||||
Number const a{aValue, 0};
|
||||
Number const b{bValue, 0};
|
||||
Number const quotient = a / b;
|
||||
|
||||
dec const exact = dec(aValue) / dec(bValue);
|
||||
dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent());
|
||||
dec const diff = stored - exact;
|
||||
|
||||
log << "\n"
|
||||
<< " a = " << aValue << "\n"
|
||||
<< " b = " << bValue << "\n"
|
||||
<< " exact a/b = " << fmt(exact) << "\n"
|
||||
<< " stored a/b = " << fmt(stored) << "\n"
|
||||
<< " stored - exact = " << fmt(diff)
|
||||
<< " (negative => ToNearest gave value BELOW truth)\n"
|
||||
<< " quotient.mantissa = " << quotient.mantissa() << "\n"
|
||||
<< " quotient.exponent = " << quotient.exponent() << "\n";
|
||||
log.flush();
|
||||
|
||||
// invariant: stored >= exact. Bug: stored < exact.
|
||||
switch (scale)
|
||||
{
|
||||
case MantissaRange::MantissaScale::Large:
|
||||
BEAST_EXPECT(stored >= exact);
|
||||
BEAST_EXPECT(diff < pow10(quotient.exponent()));
|
||||
break;
|
||||
|
||||
case MantissaRange::MantissaScale::LargeLegacy:
|
||||
BEAST_EXPECT(stored < exact);
|
||||
BEAST_EXPECT(diff >= -pow10(quotient.exponent()));
|
||||
break;
|
||||
|
||||
case MantissaRange::MantissaScale::Small:
|
||||
// Small mantissa doesn't have the correction for
|
||||
// dropped remainders
|
||||
BEAST_EXPECT(stored < exact);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1651,9 +1994,9 @@ public:
|
||||
testTruncate();
|
||||
testRounding();
|
||||
testInt64();
|
||||
|
||||
testUpwardRoundsDown();
|
||||
}
|
||||
// This test sets its own number range
|
||||
testUpwardRoundsDown();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <xrpld/app/main/LoadManager.h>
|
||||
#include <xrpld/app/main/NodeIdentity.h>
|
||||
#include <xrpld/app/main/NodeStoreScheduler.h>
|
||||
#include <xrpld/app/misc/DatagramMonitor.h>
|
||||
#include <xrpld/app/misc/SHAMapStore.h>
|
||||
#include <xrpld/app/misc/TxQ.h>
|
||||
#include <xrpld/app/misc/ValidatorKeys.h>
|
||||
@@ -221,7 +220,6 @@ public:
|
||||
std::unique_ptr<JobQueue> jobQueue_;
|
||||
NodeStoreScheduler nodeStoreScheduler_;
|
||||
std::unique_ptr<SHAMapStore> shaMapStore_;
|
||||
std::unique_ptr<DatagramMonitor> datagramMonitor_;
|
||||
PendingSaves pendingSaves_;
|
||||
std::optional<OpenLedger> openLedger_;
|
||||
|
||||
@@ -1000,6 +998,10 @@ public:
|
||||
JLOG(journal_.debug()) << "MasterTransaction sweep. Size before: " << oldMasterTxSize
|
||||
<< "; size after: " << masterTxCache.size();
|
||||
}
|
||||
{
|
||||
// Sweep NodeStore database cache(s), if enabled.
|
||||
getNodeStore().sweep();
|
||||
}
|
||||
{
|
||||
std::size_t const oldLedgerMasterCacheSize = getLedgerMaster().getFetchPackCacheSize();
|
||||
|
||||
@@ -1505,14 +1507,6 @@ ApplicationImp::start(bool withTimers)
|
||||
|
||||
ledgerCleaner_->start();
|
||||
perfLog_->start();
|
||||
|
||||
// Datagram monitor: UDP node-stats exporter (XDGM). Off in standalone or
|
||||
// when [datagram_monitor] has no endpoints.
|
||||
if (!config_->standalone() && !config_->DATAGRAM_MONITOR.empty())
|
||||
{
|
||||
datagramMonitor_ = std::make_unique<DatagramMonitor>(*this);
|
||||
datagramMonitor_->start();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1,919 +0,0 @@
|
||||
//
|
||||
#ifndef RIPPLE_APP_MAIN_DATAGRAMMONITOR_H_INCLUDED
|
||||
#define RIPPLE_APP_MAIN_DATAGRAMMONITOR_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/ledger/AcceptedLedger.h>
|
||||
#include <xrpld/app/ledger/InboundLedgers.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
#include <xrpl/server/NetworkOPs.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
|
||||
#include <xrpl/ledger/CachedSLEs.h>
|
||||
#include <xrpl/nodestore/Database.h>
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpl/basics/UptimeClock.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/basics/mulDiv.h>
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <netdb.h>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/socket.h>
|
||||
#if defined(__linux__)
|
||||
#include <sys/statvfs.h>
|
||||
#include <sys/sysinfo.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <ifaddrs.h>
|
||||
#include <mach/host_info.h>
|
||||
#include <mach/mach.h>
|
||||
#include <net/if.h>
|
||||
#include <net/if_dl.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Magic number for server info packets: 'XDGM' (le) Xahau DataGram Monitor
|
||||
constexpr uint32_t SERVER_INFO_MAGIC = 0x4D474458;
|
||||
constexpr uint32_t SERVER_INFO_VERSION = 1;
|
||||
|
||||
// Warning flag bits
|
||||
constexpr uint32_t WARNING_AMENDMENT_BLOCKED = 1 << 0;
|
||||
constexpr uint32_t WARNING_UNL_BLOCKED = 1 << 1;
|
||||
constexpr uint32_t WARNING_AMENDMENT_WARNED = 1 << 2;
|
||||
constexpr uint32_t WARNING_NOT_SYNCED = 1 << 3;
|
||||
|
||||
// Time window statistics for rates
|
||||
struct [[gnu::packed]] MetricRates
|
||||
{
|
||||
double rate_1m; // Average rate over last minute
|
||||
double rate_5m; // Average rate over last 5 minutes
|
||||
double rate_1h; // Average rate over last hour
|
||||
double rate_24h; // Average rate over last 24 hours
|
||||
};
|
||||
|
||||
struct AllRates
|
||||
{
|
||||
MetricRates network_in;
|
||||
MetricRates network_out;
|
||||
MetricRates disk_read;
|
||||
MetricRates disk_write;
|
||||
};
|
||||
|
||||
// Structure to represent a ledger sequence range
|
||||
struct [[gnu::packed]] LgrRange
|
||||
{
|
||||
uint32_t start;
|
||||
uint32_t end;
|
||||
};
|
||||
|
||||
// Map is returned separately since variable-length data
|
||||
// shouldn't be included in network structures
|
||||
using ObjectCountMap = std::vector<std::pair<std::basic_string<char>, int>>;
|
||||
|
||||
struct [[gnu::packed]] DebugCounters
|
||||
{
|
||||
// Database metrics
|
||||
std::uint64_t dbKBTotal{0};
|
||||
std::uint64_t dbKBLedger{0};
|
||||
std::uint64_t dbKBTransaction{0};
|
||||
std::uint64_t localTxCount{0};
|
||||
|
||||
// Basic metrics
|
||||
std::uint32_t writeLoad{0};
|
||||
std::int32_t historicalPerMinute{0};
|
||||
|
||||
// Cache metrics
|
||||
std::uint32_t sleHitRate{0}; // Stored as fixed point, multiplied by 1000
|
||||
std::uint32_t ledgerHitRate{
|
||||
0}; // Stored as fixed point, multiplied by 1000
|
||||
std::uint32_t alSize{0};
|
||||
std::uint32_t alHitRate{0}; // Stored as fixed point, multiplied by 1000
|
||||
std::int32_t fullbelowSize{0};
|
||||
std::uint32_t treenodeCacheSize{0};
|
||||
std::uint32_t treenodeTrackSize{0};
|
||||
|
||||
// Node store metrics
|
||||
std::uint64_t nodeWriteCount{0};
|
||||
std::uint64_t nodeWriteSize{0};
|
||||
std::uint64_t nodeFetchCount{0};
|
||||
std::uint64_t nodeFetchHitCount{0};
|
||||
std::uint64_t nodeFetchSize{0};
|
||||
};
|
||||
|
||||
// Core server metrics in the fixed header
|
||||
struct [[gnu::packed]] ServerInfoHeader
|
||||
{
|
||||
// Fixed header fields come first
|
||||
uint32_t magic; // Magic number to identify packet type
|
||||
uint32_t version; // Protocol version number
|
||||
uint32_t network_id; // Network ID from config
|
||||
uint32_t server_state; // Operating mode as enum
|
||||
uint32_t peer_count; // Number of connected peers
|
||||
uint32_t node_size; // Size category (0=tiny through 4=huge)
|
||||
uint32_t cpu_cores; // CPU core count
|
||||
uint32_t ledger_range_count; // Number of range entries
|
||||
uint32_t warning_flags; // Warning flags (reduced size)
|
||||
|
||||
uint32_t padding_1; // padding for alignment
|
||||
|
||||
// 64-bit metrics
|
||||
uint64_t timestamp; // System time in microseconds
|
||||
uint64_t uptime; // Server uptime in seconds
|
||||
uint64_t io_latency_us; // IO latency in microseconds
|
||||
uint64_t validation_quorum; // Validation quorum count
|
||||
uint64_t fetch_pack_size; // Size of fetch pack cache
|
||||
uint64_t proposer_count; // Number of proposers in last close
|
||||
uint64_t converge_time_ms; // Last convergence time in ms
|
||||
uint64_t load_factor; // Load factor (scaled by 1M)
|
||||
uint64_t load_base; // Load base value
|
||||
uint64_t reserve_base; // Reserve base amount
|
||||
uint64_t reserve_inc; // Reserve increment amount
|
||||
uint64_t ledger_seq; // Latest ledger sequence
|
||||
|
||||
// Fixed-size byte arrays
|
||||
uint8_t ledger_hash[32]; // Latest ledger hash
|
||||
uint8_t node_public_key[33]; // Node's public key
|
||||
uint8_t padding2[7]; // Padding to maintain 8-byte alignment
|
||||
uint8_t version_string[32];
|
||||
|
||||
// System metrics
|
||||
uint64_t process_memory_pages; // Process memory usage in bytes
|
||||
uint64_t system_memory_total; // Total system memory in bytes
|
||||
uint64_t system_memory_free; // Free system memory in bytes
|
||||
uint64_t system_memory_used; // Used system memory in bytes
|
||||
uint64_t system_disk_total; // Total disk space in bytes
|
||||
uint64_t system_disk_free; // Free disk space in bytes
|
||||
uint64_t system_disk_used; // Used disk space in bytes
|
||||
uint64_t io_wait_time; // IO wait time in milliseconds
|
||||
double load_avg_1min; // 1 minute load average
|
||||
double load_avg_5min; // 5 minute load average
|
||||
double load_avg_15min; // 15 minute load average
|
||||
|
||||
// State transition metrics
|
||||
uint64_t state_transitions[5]; // Count for each operating mode
|
||||
uint64_t state_durations[5]; // Duration in each mode
|
||||
uint64_t initial_sync_us; // Initial sync duration
|
||||
|
||||
// Network and disk rates remain unchanged
|
||||
struct
|
||||
{
|
||||
MetricRates network_in;
|
||||
MetricRates network_out;
|
||||
MetricRates disk_read;
|
||||
MetricRates disk_write;
|
||||
} rates;
|
||||
|
||||
DebugCounters dbg_counters;
|
||||
};
|
||||
|
||||
// System metrics collected for rate calculations
|
||||
struct SystemMetrics
|
||||
{
|
||||
uint64_t timestamp; // When metrics were collected
|
||||
uint64_t network_bytes_in; // Current total bytes in
|
||||
uint64_t network_bytes_out; // Current total bytes out
|
||||
uint64_t disk_bytes_read; // Current total bytes read
|
||||
uint64_t disk_bytes_written; // Current total bytes written
|
||||
};
|
||||
|
||||
class MetricsTracker
|
||||
{
|
||||
private:
|
||||
static constexpr size_t SAMPLES_1M = 60; // 1 sample/second for 1 minute
|
||||
static constexpr size_t SAMPLES_5M = 300; // 1 sample/second for 5 minutes
|
||||
static constexpr size_t SAMPLES_1H = 3600; // 1 sample/second for 1 hour
|
||||
static constexpr size_t SAMPLES_24H = 1440; // 1 sample/minute for 24 hours
|
||||
|
||||
std::vector<SystemMetrics> samples_1m{SAMPLES_1M};
|
||||
std::vector<SystemMetrics> samples_5m{SAMPLES_5M};
|
||||
std::vector<SystemMetrics> samples_1h{SAMPLES_1H};
|
||||
std::vector<SystemMetrics> samples_24h{SAMPLES_24H};
|
||||
|
||||
size_t index_1m{0}, index_5m{0}, index_1h{0}, index_24h{0};
|
||||
std::chrono::system_clock::time_point last_24h_sample{};
|
||||
|
||||
double
|
||||
calculateRate(
|
||||
const SystemMetrics& current,
|
||||
const std::vector<SystemMetrics>& samples,
|
||||
size_t current_index,
|
||||
size_t max_samples,
|
||||
bool is_24h_window,
|
||||
std::function<uint64_t(const SystemMetrics&)> metric_getter)
|
||||
{
|
||||
// If we don't have at least 2 samples, the rate is 0
|
||||
if (current_index < 2)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Calculate time window based on the window type
|
||||
uint64_t expected_window_micros;
|
||||
if (is_24h_window)
|
||||
{
|
||||
expected_window_micros =
|
||||
24ULL * 60ULL * 60ULL * 1000000ULL; // 24 hours in microseconds
|
||||
}
|
||||
else
|
||||
{
|
||||
expected_window_micros = max_samples *
|
||||
1000000ULL; // window in seconds * 1,000,000 for microseconds
|
||||
}
|
||||
|
||||
// For any window where we don't have full data, we should scale the
|
||||
// rate based on the actual time we have data for
|
||||
uint64_t actual_window_micros =
|
||||
current.timestamp - samples[0].timestamp;
|
||||
double window_scale = std::min(
|
||||
1.0,
|
||||
static_cast<double>(actual_window_micros) / expected_window_micros);
|
||||
|
||||
// Get the oldest valid sample
|
||||
size_t oldest_index = (current_index >= max_samples)
|
||||
? ((current_index + 1) % max_samples)
|
||||
: 0;
|
||||
const auto& oldest = samples[oldest_index];
|
||||
|
||||
double elapsed = actual_window_micros /
|
||||
1000000.0; // Convert microseconds to seconds
|
||||
|
||||
// Ensure we have a meaningful time difference
|
||||
if (elapsed < 0.001)
|
||||
{ // Less than 1ms difference
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
uint64_t current_value = metric_getter(current);
|
||||
uint64_t oldest_value = metric_getter(oldest);
|
||||
|
||||
// Handle counter wraparound
|
||||
uint64_t diff = (current_value >= oldest_value)
|
||||
? (current_value - oldest_value)
|
||||
: (std::numeric_limits<uint64_t>::max() - oldest_value +
|
||||
current_value + 1);
|
||||
|
||||
// Calculate the rate and scale it based on our window coverage
|
||||
return (static_cast<double>(diff) / elapsed) * window_scale;
|
||||
}
|
||||
|
||||
MetricRates
|
||||
calculateMetricRates(
|
||||
const SystemMetrics& current,
|
||||
std::function<uint64_t(const SystemMetrics&)> metric_getter)
|
||||
{
|
||||
MetricRates rates;
|
||||
rates.rate_1m = calculateRate(
|
||||
current, samples_1m, index_1m, SAMPLES_1M, false, metric_getter);
|
||||
rates.rate_5m = calculateRate(
|
||||
current, samples_5m, index_5m, SAMPLES_5M, false, metric_getter);
|
||||
rates.rate_1h = calculateRate(
|
||||
current, samples_1h, index_1h, SAMPLES_1H, false, metric_getter);
|
||||
rates.rate_24h = calculateRate(
|
||||
current, samples_24h, index_24h, SAMPLES_24H, true, metric_getter);
|
||||
return rates;
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
addSample(const SystemMetrics& metrics)
|
||||
{
|
||||
auto now = std::chrono::system_clock::now();
|
||||
|
||||
// Update 1-minute window (every second)
|
||||
samples_1m[index_1m++ % SAMPLES_1M] = metrics;
|
||||
|
||||
// Update 5-minute window (every second)
|
||||
samples_5m[index_5m++ % SAMPLES_5M] = metrics;
|
||||
|
||||
// Update 1-hour window (every second)
|
||||
samples_1h[index_1h++ % SAMPLES_1H] = metrics;
|
||||
|
||||
// Update 24-hour window (every minute)
|
||||
if (last_24h_sample + std::chrono::minutes(1) <= now)
|
||||
{
|
||||
samples_24h[index_24h++ % SAMPLES_24H] = metrics;
|
||||
last_24h_sample = now;
|
||||
}
|
||||
}
|
||||
|
||||
AllRates
|
||||
getRates(const SystemMetrics& current)
|
||||
{
|
||||
AllRates rates;
|
||||
rates.network_in = calculateMetricRates(
|
||||
current, [](const SystemMetrics& m) { return m.network_bytes_in; });
|
||||
rates.network_out = calculateMetricRates(
|
||||
current,
|
||||
[](const SystemMetrics& m) { return m.network_bytes_out; });
|
||||
rates.disk_read = calculateMetricRates(
|
||||
current, [](const SystemMetrics& m) { return m.disk_bytes_read; });
|
||||
rates.disk_write = calculateMetricRates(
|
||||
current,
|
||||
[](const SystemMetrics& m) { return m.disk_bytes_written; });
|
||||
return rates;
|
||||
}
|
||||
};
|
||||
|
||||
class DatagramMonitor
|
||||
{
|
||||
private:
|
||||
Application& app_;
|
||||
beast::Journal j_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::thread monitor_thread_;
|
||||
MetricsTracker metrics_tracker_;
|
||||
|
||||
struct EndpointInfo
|
||||
{
|
||||
std::string ip;
|
||||
uint16_t port;
|
||||
bool is_ipv6;
|
||||
};
|
||||
EndpointInfo
|
||||
parseEndpoint(std::string const& endpoint)
|
||||
{
|
||||
auto space_pos = endpoint.find(' ');
|
||||
if (space_pos == std::string::npos)
|
||||
throw std::runtime_error("Invalid endpoint format");
|
||||
|
||||
EndpointInfo info;
|
||||
info.ip = endpoint.substr(0, space_pos);
|
||||
info.port = std::stoi(endpoint.substr(space_pos + 1));
|
||||
info.is_ipv6 = info.ip.find(':') != std::string::npos;
|
||||
return info;
|
||||
}
|
||||
|
||||
int
|
||||
createSocket(EndpointInfo const& endpoint)
|
||||
{
|
||||
int sock = socket(endpoint.is_ipv6 ? AF_INET6 : AF_INET, SOCK_DGRAM, 0);
|
||||
if (sock < 0)
|
||||
throw std::runtime_error("Failed to create socket");
|
||||
return sock;
|
||||
}
|
||||
|
||||
void
|
||||
sendPacket(
|
||||
int sock,
|
||||
EndpointInfo const& endpoint,
|
||||
std::vector<uint8_t> const& buffer)
|
||||
{
|
||||
struct sockaddr_storage addr;
|
||||
socklen_t addr_len;
|
||||
|
||||
if (endpoint.is_ipv6)
|
||||
{
|
||||
struct sockaddr_in6* addr6 =
|
||||
reinterpret_cast<struct sockaddr_in6*>(&addr);
|
||||
addr6->sin6_family = AF_INET6;
|
||||
addr6->sin6_port = htons(endpoint.port);
|
||||
inet_pton(AF_INET6, endpoint.ip.c_str(), &addr6->sin6_addr);
|
||||
addr_len = sizeof(struct sockaddr_in6);
|
||||
}
|
||||
else
|
||||
{
|
||||
struct sockaddr_in* addr4 =
|
||||
reinterpret_cast<struct sockaddr_in*>(&addr);
|
||||
addr4->sin_family = AF_INET;
|
||||
addr4->sin_port = htons(endpoint.port);
|
||||
inet_pton(AF_INET, endpoint.ip.c_str(), &addr4->sin_addr);
|
||||
addr_len = sizeof(struct sockaddr_in);
|
||||
}
|
||||
|
||||
sendto(
|
||||
sock,
|
||||
buffer.data(),
|
||||
buffer.size(),
|
||||
0,
|
||||
reinterpret_cast<struct sockaddr*>(&addr),
|
||||
addr_len);
|
||||
}
|
||||
|
||||
// Returns both the counters and object count map separately
|
||||
std::pair<DebugCounters, ObjectCountMap>
|
||||
getDebugCounters()
|
||||
{
|
||||
DebugCounters counters;
|
||||
ObjectCountMap objectCounts =
|
||||
CountedObjects::getInstance().getCounts(1);
|
||||
|
||||
// Database metrics if applicable
|
||||
if (app_.config().useTxTables())
|
||||
{
|
||||
auto const db =
|
||||
dynamic_cast<SQLiteDatabase*>(&app_.getRelationalDatabase());
|
||||
if (!db)
|
||||
Throw<std::runtime_error>("Failed to get relational database");
|
||||
|
||||
if (auto dbKB = db->getKBUsedAll())
|
||||
counters.dbKBTotal = dbKB;
|
||||
if (auto dbKB = db->getKBUsedLedger())
|
||||
counters.dbKBLedger = dbKB;
|
||||
if (auto dbKB = db->getKBUsedTransaction())
|
||||
counters.dbKBTransaction = dbKB;
|
||||
if (auto count = app_.getOPs().getLocalTxCount())
|
||||
counters.localTxCount = count;
|
||||
}
|
||||
|
||||
// Basic metrics
|
||||
counters.writeLoad = app_.getNodeStore().getWriteLoad();
|
||||
counters.historicalPerMinute =
|
||||
static_cast<std::int32_t>(app_.getInboundLedgers().fetchRate());
|
||||
|
||||
// Cache metrics - convert floating point rates to fixed point
|
||||
counters.sleHitRate = 0; // TODO: SLE cache hit-rate accessor absent on this fork
|
||||
counters.ledgerHitRate = static_cast<std::uint32_t>(
|
||||
app_.getLedgerMaster().getCacheHitRate() * 1000);
|
||||
counters.alSize = app_.getAcceptedLedgerCache().size();
|
||||
counters.alHitRate = static_cast<std::uint32_t>(
|
||||
app_.getAcceptedLedgerCache().getHitRate() * 1000);
|
||||
counters.fullbelowSize = static_cast<std::int32_t>(
|
||||
app_.getNodeFamily().getFullBelowCache()->size());
|
||||
counters.treenodeCacheSize =
|
||||
app_.getNodeFamily().getTreeNodeCache()->getCacheSize();
|
||||
counters.treenodeTrackSize =
|
||||
app_.getNodeFamily().getTreeNodeCache()->getTrackSize();
|
||||
|
||||
// Get regular node store metrics
|
||||
counters.nodeWriteCount = app_.getNodeStore().getStoreCount();
|
||||
counters.nodeWriteSize = app_.getNodeStore().getStoreSize();
|
||||
counters.nodeFetchCount = app_.getNodeStore().getFetchTotalCount();
|
||||
counters.nodeFetchHitCount = app_.getNodeStore().getFetchHitCount();
|
||||
counters.nodeFetchSize = app_.getNodeStore().getFetchSize();
|
||||
|
||||
return {counters, objectCounts};
|
||||
}
|
||||
|
||||
uint32_t
|
||||
getPhysicalCPUCount()
|
||||
{
|
||||
static uint32_t count = 0;
|
||||
if (count > 0)
|
||||
return count;
|
||||
|
||||
#if defined(__linux__)
|
||||
try
|
||||
{
|
||||
std::ifstream cpuinfo("/proc/cpuinfo");
|
||||
if (!cpuinfo)
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "Unable to open file: /proc/cpuinfo";
|
||||
return count;
|
||||
}
|
||||
std::string line;
|
||||
std::set<std::string> physical_ids;
|
||||
std::string current_physical_id;
|
||||
|
||||
while (std::getline(cpuinfo, line))
|
||||
{
|
||||
if (line.find("core id") != std::string::npos)
|
||||
{
|
||||
current_physical_id = line.substr(line.find(":") + 1);
|
||||
// Trim whitespace
|
||||
current_physical_id.erase(
|
||||
0, current_physical_id.find_first_not_of(" \t"));
|
||||
current_physical_id.erase(
|
||||
current_physical_id.find_last_not_of(" \t") + 1);
|
||||
physical_ids.insert(current_physical_id);
|
||||
}
|
||||
}
|
||||
|
||||
count = physical_ids.size();
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "Error getting CPU count: " << e.what();
|
||||
}
|
||||
|
||||
// Return at least 1 if we couldn't determine the count
|
||||
return count > 0 ? count : (count = 1);
|
||||
#elif defined(__APPLE__)
|
||||
int value = 0;
|
||||
size_t size = sizeof(value);
|
||||
if (sysctlbyname("hw.physicalcpu", &value, &size, NULL, 0) == 0)
|
||||
count = value;
|
||||
return count > 0 ? count : (count = 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
SystemMetrics
|
||||
collectSystemMetrics()
|
||||
{
|
||||
SystemMetrics metrics{};
|
||||
metrics.timestamp =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
|
||||
#if defined(__linux__)
|
||||
// Network stats collection
|
||||
try
|
||||
{
|
||||
std::ifstream net_file("/proc/net/dev");
|
||||
if (!net_file)
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "Unable to open file /proc/net/dev";
|
||||
return metrics;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
uint64_t total_bytes_in = 0, total_bytes_out = 0;
|
||||
|
||||
// Skip header lines
|
||||
std::getline(net_file, line); // Inter-| Receive...
|
||||
std::getline(net_file, line); // face |bytes...
|
||||
|
||||
while (std::getline(net_file, line))
|
||||
{
|
||||
if (line.find(':') != std::string::npos)
|
||||
{
|
||||
std::string interface = line.substr(0, line.find(':'));
|
||||
interface =
|
||||
interface.substr(interface.find_first_not_of(" \t"));
|
||||
interface = interface.substr(
|
||||
0, interface.find_last_not_of(" \t") + 1);
|
||||
|
||||
// Skip loopback interface
|
||||
if (interface == "lo")
|
||||
continue;
|
||||
|
||||
uint64_t bytes_in, bytes_out;
|
||||
std::istringstream iss(line.substr(line.find(':') + 1));
|
||||
iss >> bytes_in; // First field after : is bytes_in
|
||||
for (int i = 0; i < 8; ++i)
|
||||
iss >> std::ws; // Skip 8 fields
|
||||
iss >> bytes_out; // 9th field is bytes_out
|
||||
|
||||
total_bytes_in += bytes_in;
|
||||
total_bytes_out += bytes_out;
|
||||
}
|
||||
}
|
||||
metrics.network_bytes_in = total_bytes_in;
|
||||
metrics.network_bytes_out = total_bytes_out;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "Error collecting network stats: " << e.what();
|
||||
}
|
||||
|
||||
// Disk stats collection
|
||||
try
|
||||
{
|
||||
std::ifstream disk_file("/proc/diskstats");
|
||||
if (!disk_file)
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "Unable to open file: /proc/diskstats";
|
||||
return metrics;
|
||||
}
|
||||
std::string line;
|
||||
uint64_t total_bytes_read = 0, total_bytes_written = 0;
|
||||
|
||||
while (std::getline(disk_file, line))
|
||||
{
|
||||
unsigned int major, minor;
|
||||
char dev_name[32];
|
||||
uint64_t reads, read_sectors, writes, write_sectors;
|
||||
|
||||
if (sscanf(
|
||||
line.c_str(),
|
||||
"%u %u %31s %lu %*u %lu %*u %lu %*u %lu",
|
||||
&major,
|
||||
&minor,
|
||||
dev_name,
|
||||
&reads,
|
||||
&read_sectors,
|
||||
&writes,
|
||||
&write_sectors) == 7)
|
||||
{
|
||||
// Only process physical devices
|
||||
std::string device_name(dev_name);
|
||||
if (device_name.substr(0, 3) == "dm-" ||
|
||||
device_name.substr(0, 4) == "loop" ||
|
||||
device_name.substr(0, 3) == "ram")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip partitions (usually have a number at the end)
|
||||
if (std::isdigit(device_name.back()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64_t bytes_read = read_sectors * 512;
|
||||
uint64_t bytes_written = write_sectors * 512;
|
||||
|
||||
total_bytes_read += bytes_read;
|
||||
total_bytes_written += bytes_written;
|
||||
}
|
||||
}
|
||||
metrics.disk_bytes_read = total_bytes_read;
|
||||
metrics.disk_bytes_written = total_bytes_written;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "Error collecting disk stats: " << e.what();
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
// Network stats collection
|
||||
try
|
||||
{
|
||||
struct ifaddrs* ifap;
|
||||
if (getifaddrs(&ifap) == 0)
|
||||
{
|
||||
uint64_t total_bytes_in = 0, total_bytes_out = 0;
|
||||
for (struct ifaddrs* ifa = ifap; ifa; ifa = ifa->ifa_next)
|
||||
{
|
||||
if (ifa->ifa_addr != NULL &&
|
||||
ifa->ifa_addr->sa_family == AF_LINK)
|
||||
{
|
||||
struct if_data* ifd = (struct if_data*)ifa->ifa_data;
|
||||
if (ifd != NULL)
|
||||
{
|
||||
// Skip loopback interface
|
||||
if (strcmp(ifa->ifa_name, "lo0") == 0)
|
||||
continue;
|
||||
|
||||
total_bytes_in += ifd->ifi_ibytes;
|
||||
total_bytes_out += ifd->ifi_obytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
freeifaddrs(ifap);
|
||||
|
||||
metrics.network_bytes_in = total_bytes_in;
|
||||
metrics.network_bytes_out = total_bytes_out;
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "Error collecting network stats: " << e.what();
|
||||
}
|
||||
|
||||
// Disk stats collection
|
||||
// Disk IO stats are not easily accessible in macOS.
|
||||
// We'll set these values to zero for now.
|
||||
metrics.disk_bytes_read = 0;
|
||||
metrics.disk_bytes_written = 0;
|
||||
#endif
|
||||
return metrics;
|
||||
}
|
||||
|
||||
std::vector<uint8_t>
|
||||
generateServerInfo()
|
||||
{
|
||||
auto& ops = app_.getOPs();
|
||||
auto& ledgerMaster = app_.getLedgerMaster();
|
||||
|
||||
auto currentMetrics = collectSystemMetrics();
|
||||
metrics_tracker_.addSample(currentMetrics);
|
||||
|
||||
// Slimmed for this fork (3.2.0-b0): ledger ranges, DB debug-counters and
|
||||
// the object-count map are omitted (divergent accessors). The packet is
|
||||
// just the fixed header with core node + OS metrics.
|
||||
std::vector<uint8_t> buffer(sizeof(ServerInfoHeader));
|
||||
auto* header = reinterpret_cast<ServerInfoHeader*>(buffer.data());
|
||||
memset(header, 0, sizeof(ServerInfoHeader));
|
||||
|
||||
header->magic = SERVER_INFO_MAGIC;
|
||||
header->version = SERVER_INFO_VERSION;
|
||||
header->network_id = app_.config().networkId;
|
||||
header->timestamp =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
header->uptime = UptimeClock::now().time_since_epoch().count();
|
||||
header->io_latency_us = app_.getIOLatency().count();
|
||||
header->validation_quorum = 0; // TODO: fork validator-list accessor
|
||||
header->peer_count = app_.getOverlay().size();
|
||||
header->node_size = app_.config().nodeSize;
|
||||
|
||||
auto const [counters, mode, start, initialSync] =
|
||||
ops.getStateAccountingData();
|
||||
for (size_t i = 0; i < 5; ++i)
|
||||
{
|
||||
header->state_transitions[i] = counters[i].transitions;
|
||||
header->state_durations[i] = counters[i].dur.count();
|
||||
}
|
||||
header->initial_sync_us = initialSync;
|
||||
|
||||
if (ops.isAmendmentBlocked())
|
||||
header->warning_flags |= WARNING_AMENDMENT_BLOCKED;
|
||||
if (ops.isUNLBlocked())
|
||||
header->warning_flags |= WARNING_UNL_BLOCKED;
|
||||
if (ops.isAmendmentWarned())
|
||||
header->warning_flags |= WARNING_AMENDMENT_WARNED;
|
||||
if (ops.getOperatingMode() != OperatingMode::FULL)
|
||||
header->warning_flags |= WARNING_NOT_SYNCED;
|
||||
|
||||
// Consensus timing is private on this fork's NetworkOPs; zeroed.
|
||||
header->proposer_count = 0;
|
||||
header->converge_time_ms = 0;
|
||||
|
||||
auto const fp = ledgerMaster.getFetchPackCacheSize();
|
||||
if (fp != 0)
|
||||
header->fetch_pack_size = fp;
|
||||
|
||||
// Load factor (server only; fee-escalation term omitted on this fork).
|
||||
header->load_factor =
|
||||
static_cast<std::uint64_t>(app_.getFeeTrack().getLoadFactor());
|
||||
header->load_base = app_.getFeeTrack().getLoadBase();
|
||||
|
||||
#if defined(__linux__)
|
||||
// Get system info using sysinfo
|
||||
struct sysinfo si;
|
||||
if (sysinfo(&si) == 0)
|
||||
{
|
||||
header->system_memory_total = si.totalram * si.mem_unit;
|
||||
header->system_memory_free = si.freeram * si.mem_unit;
|
||||
header->system_memory_used =
|
||||
header->system_memory_total - header->system_memory_free;
|
||||
header->load_avg_1min = si.loads[0] / (float)(1 << SI_LOAD_SHIFT);
|
||||
header->load_avg_5min = si.loads[1] / (float)(1 << SI_LOAD_SHIFT);
|
||||
header->load_avg_15min = si.loads[2] / (float)(1 << SI_LOAD_SHIFT);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
// Get total physical memory
|
||||
int64_t physical_memory;
|
||||
size_t length = sizeof(physical_memory);
|
||||
if (sysctlbyname("hw.memsize", &physical_memory, &length, NULL, 0) == 0)
|
||||
{
|
||||
header->system_memory_total = physical_memory;
|
||||
}
|
||||
|
||||
// Get free and used memory
|
||||
vm_statistics_data_t vm_stats;
|
||||
mach_msg_type_number_t count = HOST_VM_INFO_COUNT;
|
||||
if (host_statistics(
|
||||
mach_host_self(),
|
||||
HOST_VM_INFO,
|
||||
(host_info_t)&vm_stats,
|
||||
&count) == KERN_SUCCESS)
|
||||
{
|
||||
uint64_t page_size;
|
||||
length = sizeof(page_size);
|
||||
sysctlbyname("hw.pagesize", &page_size, &length, NULL, 0);
|
||||
|
||||
header->system_memory_free =
|
||||
(uint64_t)vm_stats.free_count * page_size;
|
||||
header->system_memory_used =
|
||||
header->system_memory_total - header->system_memory_free;
|
||||
}
|
||||
|
||||
// Get load averages
|
||||
double loadavg[3];
|
||||
if (getloadavg(loadavg, 3) == 3)
|
||||
{
|
||||
header->load_avg_1min = loadavg[0];
|
||||
header->load_avg_5min = loadavg[1];
|
||||
header->load_avg_15min = loadavg[2];
|
||||
}
|
||||
#endif
|
||||
|
||||
// Get process memory usage
|
||||
struct rusage usage;
|
||||
getrusage(RUSAGE_SELF, &usage);
|
||||
header->process_memory_pages = usage.ru_maxrss;
|
||||
|
||||
// Get disk usage
|
||||
#if defined(__linux__)
|
||||
struct statvfs fs;
|
||||
if (statvfs("/", &fs) == 0)
|
||||
{
|
||||
header->system_disk_total = fs.f_blocks * fs.f_frsize;
|
||||
header->system_disk_free = fs.f_bfree * fs.f_frsize;
|
||||
header->system_disk_used =
|
||||
header->system_disk_total - header->system_disk_free;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
struct statfs fs;
|
||||
if (statfs("/", &fs) == 0)
|
||||
{
|
||||
header->system_disk_total = fs.f_blocks * fs.f_bsize;
|
||||
header->system_disk_free = fs.f_bfree * fs.f_bsize;
|
||||
header->system_disk_used =
|
||||
header->system_disk_total - header->system_disk_free;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Get CPU core count
|
||||
header->cpu_cores = getPhysicalCPUCount();
|
||||
|
||||
// Get rate statistics
|
||||
auto rates = metrics_tracker_.getRates(currentMetrics);
|
||||
header->rates.network_in = rates.network_in;
|
||||
header->rates.network_out = rates.network_out;
|
||||
header->rates.disk_read = rates.disk_read;
|
||||
header->rates.disk_write = rates.disk_write;
|
||||
|
||||
// Ledger height via a stable accessor (this fork's Ledger lacks info()).
|
||||
header->ledger_seq = ledgerMaster.getValidLedgerIndex();
|
||||
header->reserve_base = app_.config().fees.accountReserve.drops();
|
||||
header->reserve_inc = app_.config().fees.ownerReserve.drops();
|
||||
|
||||
// Node public key + version string.
|
||||
auto const& nodeKey = app_.nodeIdentity().first;
|
||||
std::memcpy(header->node_public_key, nodeKey.data(), 33);
|
||||
memset(&header->version_string, 0, 32);
|
||||
memcpy(
|
||||
&header->version_string,
|
||||
BuildInfo::getVersionString().c_str(),
|
||||
BuildInfo::getVersionString().size() > 32
|
||||
? 32
|
||||
: BuildInfo::getVersionString().size());
|
||||
|
||||
header->ledger_range_count = 0;
|
||||
return buffer;
|
||||
}
|
||||
void
|
||||
monitorThread()
|
||||
{
|
||||
std::vector<std::pair<EndpointInfo, int>> endpoints;
|
||||
|
||||
for (auto const& epStr : app_.config().DATAGRAM_MONITOR)
|
||||
{
|
||||
auto endpoint = parseEndpoint(epStr);
|
||||
endpoints.push_back(
|
||||
std::make_pair(endpoint, createSocket(endpoint)));
|
||||
}
|
||||
|
||||
while (running_)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto info = generateServerInfo();
|
||||
for (auto const& ep : endpoints)
|
||||
{
|
||||
sendPacket(ep.second, ep.first, info);
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
// Log error but continue monitoring
|
||||
JLOG(j_.error())
|
||||
<< "Server info monitor error: " << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& ep : endpoints)
|
||||
{
|
||||
close(ep.second);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
DatagramMonitor(Application& app) : app_(app), j_(beast::Journal::getNullSink())
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
start()
|
||||
{
|
||||
if (!running_.exchange(true))
|
||||
{
|
||||
monitor_thread_ =
|
||||
std::thread(&DatagramMonitor::monitorThread, this);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
stop()
|
||||
{
|
||||
if (running_.exchange(false))
|
||||
{
|
||||
if (monitor_thread_.joinable())
|
||||
monitor_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
~DatagramMonitor()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
};
|
||||
} // namespace xrpl
|
||||
#endif
|
||||
@@ -346,9 +346,6 @@ public:
|
||||
OperatingMode
|
||||
getOperatingMode() const override;
|
||||
|
||||
StateAccountingData
|
||||
getStateAccountingData() override;
|
||||
|
||||
std::string
|
||||
strOperatingMode(OperatingMode const mode, bool const admin) const override;
|
||||
|
||||
@@ -918,16 +915,6 @@ NetworkOPsImp::getOperatingMode() const
|
||||
return mode_;
|
||||
}
|
||||
|
||||
NetworkOPs::StateAccountingData
|
||||
NetworkOPsImp::getStateAccountingData()
|
||||
{
|
||||
auto const data = accounting_.getCounterData();
|
||||
std::array<NetworkOPs::AccountingCounter, 5> out;
|
||||
for (std::size_t i = 0; i < out.size(); ++i)
|
||||
out[i] = {data.counters[i].transitions, data.counters[i].dur};
|
||||
return {out, data.mode, data.start, data.initialSyncUs};
|
||||
}
|
||||
|
||||
inline std::string
|
||||
NetworkOPsImp::strOperatingMode(bool const admin /* = false */) const
|
||||
{
|
||||
|
||||
@@ -165,6 +165,22 @@ std::unique_ptr<NodeStore::Database>
|
||||
SHAMapStoreImp::makeNodeStore(int readThreads)
|
||||
{
|
||||
auto nscfg = app_.config().section(ConfigSection::nodeDatabase());
|
||||
|
||||
// Provide default values.
|
||||
if (!nscfg.exists("cache_size"))
|
||||
{
|
||||
nscfg.set(
|
||||
"cache_size",
|
||||
std::to_string(app_.config().getValueFor(SizedItem::TreeCacheSize, std::nullopt)));
|
||||
}
|
||||
|
||||
if (!nscfg.exists("cache_age"))
|
||||
{
|
||||
nscfg.set(
|
||||
"cache_age",
|
||||
std::to_string(app_.config().getValueFor(SizedItem::TreeCacheAge, std::nullopt)));
|
||||
}
|
||||
|
||||
std::unique_ptr<NodeStore::Database> db;
|
||||
|
||||
if (deleteInterval_ != 0u)
|
||||
@@ -254,6 +270,8 @@ SHAMapStoreImp::run()
|
||||
LedgerIndex lastRotated = stateDb_.getState().lastRotated;
|
||||
netOPs_ = &app_.getOPs();
|
||||
ledgerMaster_ = &app_.getLedgerMaster();
|
||||
fullBelowCache_ = &(*app_.getNodeFamily().getFullBelowCache());
|
||||
treeNodeCache_ = &(*app_.getNodeFamily().getTreeNodeCache());
|
||||
|
||||
if (advisoryDelete_)
|
||||
canDelete_ = stateDb_.getCanDelete();
|
||||
@@ -542,16 +560,16 @@ SHAMapStoreImp::clearCaches(LedgerIndex validatedSeq)
|
||||
// Also clear the FullBelowCache so its generation counter is bumped.
|
||||
// This prevents stale "full below" markers from persisting across
|
||||
// backend rotation/online deletion and interfering with SHAMap sync.
|
||||
app_.getNodeFamily().getFullBelowCache()->clear();
|
||||
fullBelowCache_->clear();
|
||||
}
|
||||
|
||||
void
|
||||
SHAMapStoreImp::freshenCaches()
|
||||
{
|
||||
if (freshenCache(*app_.getNodeFamily().getTreeNodeCache()))
|
||||
if (freshenCache(*treeNodeCache_))
|
||||
return;
|
||||
if (freshenCache(app_.getMasterTransaction().getCache()))
|
||||
return;
|
||||
|
||||
freshenCache(app_.getMasterTransaction().getCache());
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <xrpl/nodestore/Scheduler.h>
|
||||
#include <xrpl/rdb/DatabaseCon.h>
|
||||
#include <xrpl/server/State.h>
|
||||
#include <xrpl/shamap/FullBelowCache.h>
|
||||
#include <xrpl/shamap/TreeNodeCache.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -93,6 +95,8 @@ private:
|
||||
// as of run() or before
|
||||
NetworkOPs* netOPs_ = nullptr;
|
||||
LedgerMaster* ledgerMaster_ = nullptr;
|
||||
FullBelowCache* fullBelowCache_ = nullptr;
|
||||
TreeNodeCache* treeNodeCache_ = nullptr;
|
||||
|
||||
static constexpr auto kNodeStoreName = "NodeStore";
|
||||
|
||||
|
||||
@@ -134,10 +134,6 @@ public:
|
||||
// Entries from [ips_fixed] config stanza
|
||||
std::vector<std::string> ipsFixed;
|
||||
|
||||
// Entries from [datagram_monitor]: "<IP> <port>" UDP targets the
|
||||
// DatagramMonitor sends node-stats packets to (XDGM, every 1s).
|
||||
std::vector<std::string> DATAGRAM_MONITOR;
|
||||
|
||||
StartUpType startUp = StartUpType::Normal;
|
||||
|
||||
bool startValid = false;
|
||||
|
||||
@@ -27,7 +27,6 @@ struct ConfigSection
|
||||
#define SECTION_BETA_RPC_API "beta_rpc_api"
|
||||
#define SECTION_CLUSTER_NODES "cluster_nodes"
|
||||
#define SECTION_COMPRESSION "compression"
|
||||
#define SECTION_DATAGRAM_MONITOR "datagram_monitor"
|
||||
#define SECTION_DEBUG_LOGFILE "debug_logfile"
|
||||
#define SECTION_ELB_SUPPORT "elb_support"
|
||||
#define SECTION_FEE_DEFAULT "fee_default"
|
||||
|
||||
@@ -483,9 +483,6 @@ Config::loadFromString(std::string const& fileContents)
|
||||
if (auto s = getIniFileSection(secConfig, SECTION_IPS_FIXED))
|
||||
ipsFixed = *s;
|
||||
|
||||
if (auto s = getIniFileSection(secConfig, SECTION_DATAGRAM_MONITOR))
|
||||
DATAGRAM_MONITOR = *s;
|
||||
|
||||
// if the user has specified ip:port then replace : with a space.
|
||||
{
|
||||
auto replaceColons = [](std::vector<std::string>& strVec) {
|
||||
|
||||
Reference in New Issue
Block a user