Merge branch 'pratik/otel-phase4-consensus-tracing' into pratik/otel-phase5-docs-deployment

This commit is contained in:
Pratik Mankawde
2026-07-08 16:02:51 +01:00
78 changed files with 5147 additions and 5313 deletions

View File

@@ -29,6 +29,8 @@ set(test_modules
basics
crypto
json
resource
shamap
tx
protocol_autogen
telemetry

View File

@@ -0,0 +1,260 @@
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <type_traits>
#include <utility>
namespace xrpl::test {
struct BufferTest : public ::testing::Test
{
static bool
sane(Buffer const& b)
{
if (b.empty())
return b.data() == nullptr;
return b.data() != nullptr;
}
};
TEST_F(BufferTest, buffer)
{
std::uint8_t const data[] = {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a,
0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c,
0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3};
Buffer const b0;
EXPECT_TRUE(sane(b0));
EXPECT_TRUE(b0.empty());
Buffer b1{0};
EXPECT_TRUE(sane(b1));
EXPECT_TRUE(b1.empty());
std::memcpy(b1.alloc(16), data, 16);
EXPECT_TRUE(sane(b1));
EXPECT_FALSE(b1.empty());
EXPECT_EQ(b1.size(), 16);
Buffer b2{b1.size()};
EXPECT_TRUE(sane(b2));
EXPECT_FALSE(b2.empty());
EXPECT_EQ(b2.size(), b1.size());
std::memcpy(b2.data(), data + 16, 16);
Buffer b3{data, sizeof(data)};
EXPECT_TRUE(sane(b3));
EXPECT_FALSE(b3.empty());
EXPECT_EQ(b3.size(), sizeof(data));
EXPECT_EQ(std::memcmp(b3.data(), data, b3.size()), 0);
// Check equality and inequality comparisons.
// For code readability, we want to use general
// EXPECT_TRUE instead of specific EXPECT_EQ etc.
EXPECT_TRUE(b0 == b0);
EXPECT_TRUE(b0 != b1);
EXPECT_TRUE(b1 == b1);
EXPECT_TRUE(b1 != b2);
EXPECT_TRUE(b2 != b3);
// Check copy constructors and copy assignments:
{
Buffer x{b0};
EXPECT_EQ(x, b0);
EXPECT_TRUE(sane(x));
Buffer y{b1};
EXPECT_EQ(y, b1);
EXPECT_TRUE(sane(y));
x = b2;
EXPECT_EQ(x, b2);
EXPECT_TRUE(sane(x));
x = y;
EXPECT_EQ(x, y);
EXPECT_TRUE(sane(x));
y = b3;
EXPECT_EQ(y, b3);
EXPECT_TRUE(sane(y));
x = b0;
EXPECT_EQ(x, b0);
EXPECT_TRUE(sane(x));
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wself-assign-overloaded"
#endif
x = x;
EXPECT_EQ(x, b0);
EXPECT_TRUE(sane(x));
y = y;
EXPECT_EQ(y, b3);
EXPECT_TRUE(sane(y));
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
}
// Check move constructor & move assignments:
{
static_assert(std::is_nothrow_move_constructible_v<Buffer>);
static_assert(std::is_nothrow_move_assignable_v<Buffer>);
{ // Move-construct from empty buf
Buffer x;
Buffer const y{std::move(x)};
EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(sane(y));
EXPECT_TRUE(y.empty());
EXPECT_EQ(x, y); // NOLINT(bugprone-use-after-move)
}
{ // Move-construct from non-empty buf
Buffer x{b1};
Buffer const y{std::move(x)};
EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(sane(y));
EXPECT_EQ(y, b1);
}
{ // Move assign empty buf to empty buf
Buffer x;
Buffer y;
x = std::move(y);
EXPECT_TRUE(sane(x));
EXPECT_TRUE(x.empty());
EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move)
}
{ // Move assign non-empty buf to empty buf
Buffer x;
Buffer y{b1};
x = std::move(y);
EXPECT_TRUE(sane(x));
EXPECT_EQ(x, b1);
EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move)
}
{ // Move assign empty buf to non-empty buf
Buffer x{b1};
Buffer y;
x = std::move(y);
EXPECT_TRUE(sane(x));
EXPECT_TRUE(x.empty());
EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move)
}
{ // Move assign non-empty buf to non-empty buf
Buffer x{b1};
Buffer y{b2};
Buffer z{b3};
x = std::move(y);
EXPECT_TRUE(sane(x));
EXPECT_FALSE(x.empty());
EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move)
x = std::move(z);
EXPECT_TRUE(sane(x));
EXPECT_FALSE(x.empty());
EXPECT_TRUE(sane(z)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(z.empty()); // NOLINT(bugprone-use-after-move)
}
}
{
Buffer w{static_cast<Slice>(b0)};
EXPECT_TRUE(sane(w));
EXPECT_EQ(w, b0);
Buffer x{static_cast<Slice>(b1)};
EXPECT_TRUE(sane(x));
EXPECT_EQ(x, b1);
Buffer y{static_cast<Slice>(b2)};
EXPECT_TRUE(sane(y));
EXPECT_EQ(y, b2);
Buffer z{static_cast<Slice>(b3)};
EXPECT_TRUE(sane(z));
EXPECT_EQ(z, b3);
// Assign empty slice to empty buffer
w = static_cast<Slice>(b0);
EXPECT_TRUE(sane(w));
EXPECT_EQ(w, b0);
// Assign non-empty slice to empty buffer
w = static_cast<Slice>(b1);
EXPECT_TRUE(sane(w));
EXPECT_EQ(w, b1);
// Assign non-empty slice to non-empty buffer
x = static_cast<Slice>(b2);
EXPECT_TRUE(sane(x));
EXPECT_EQ(x, b2);
// Assign non-empty slice to non-empty buffer
y = static_cast<Slice>(z);
EXPECT_TRUE(sane(y));
EXPECT_EQ(y, z);
// Assign empty slice to non-empty buffer:
z = static_cast<Slice>(b0);
EXPECT_TRUE(sane(z));
EXPECT_EQ(z, b0);
}
{
auto test = [](Buffer const& b, std::size_t i) {
Buffer x{b};
// Try to allocate some number of bytes, possibly
// zero (which means clear) and sanity check
x(i);
EXPECT_TRUE(sane(x));
EXPECT_EQ(x.size(), i);
EXPECT_EQ((x.data() == nullptr), (i == 0));
// Try to allocate some more data (always non-zero)
x(i + 1);
EXPECT_TRUE(sane(x));
EXPECT_EQ(x.size(), i + 1);
EXPECT_NE(x.data(), nullptr);
// Try to clear:
x.clear();
EXPECT_TRUE(sane(x));
EXPECT_TRUE(x.empty());
EXPECT_EQ(x.data(), nullptr);
// Try to clear again:
x.clear();
EXPECT_TRUE(sane(x));
EXPECT_TRUE(x.empty());
EXPECT_EQ(x.data(), nullptr);
};
for (std::size_t i = 0; i < 16; ++i)
{
test(b0, i);
test(b1, i);
}
}
}
} // namespace xrpl::test

View File

@@ -0,0 +1,94 @@
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/ByteUtilities.h>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/system/detail/errc.hpp>
#include <boost/system/detail/error_code.hpp>
#include <gtest/gtest.h>
#include <fstream>
#include <stdexcept>
#include <string>
namespace xrpl {
namespace {
class TempFile
{
public:
explicit TempFile(boost::filesystem::path file, std::string const& contents)
: dir_(
boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path("xrpl-file-utilities-%%%%-%%%%-%%%%"))
, file_(dir_ / file)
{
boost::filesystem::create_directory(dir_);
std::ofstream output(file_.string());
if (!output)
throw std::runtime_error("Unable to create temporary test file");
output << contents;
}
~TempFile()
{
boost::system::error_code ec;
boost::filesystem::remove(file_, ec);
boost::filesystem::remove(dir_, ec);
}
[[nodiscard]] boost::filesystem::path const&
file() const
{
return file_;
}
private:
boost::filesystem::path dir_;
boost::filesystem::path file_;
};
} // namespace
TEST(FileUtilitiesTest, get_file_contents)
{
using namespace boost::system;
constexpr char const* kExpectedContents = "This file is very short. That's all we need.";
TempFile const file("test_file", "This is temporary text that should get overwritten");
error_code ec;
auto const path = file.file();
writeFileContents(ec, path, kExpectedContents);
EXPECT_FALSE(ec);
{
// Test with no max
auto const good = getFileContents(ec, path);
EXPECT_FALSE(ec);
EXPECT_EQ(good, kExpectedContents);
}
{
// Test with large max
auto const good = getFileContents(ec, path, kilobytes(1));
EXPECT_FALSE(ec);
EXPECT_EQ(good, kExpectedContents);
}
{
// Test with small max
auto const bad = getFileContents(ec, path, 16);
EXPECT_TRUE(ec && ec.value() == boost::system::errc::file_too_large);
EXPECT_TRUE(bad.empty());
}
}
} // namespace xrpl

View File

@@ -0,0 +1,243 @@
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Zero.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
#include <sstream>
#include <string>
namespace xrpl {
TEST(IOUAmountTest, zero)
{
IOUAmount const z(0, 0);
EXPECT_EQ(z.mantissa(), 0);
EXPECT_EQ(z.exponent(), -100);
EXPECT_FALSE(z);
EXPECT_EQ(z.signum(), 0);
EXPECT_EQ(z, beast::kZero);
EXPECT_EQ((z + z), z);
EXPECT_EQ((z - z), z);
EXPECT_EQ(z, -z);
IOUAmount const zz(beast::kZero);
EXPECT_EQ(z, zz);
// https://github.com/XRPLF/rippled/issues/5170
IOUAmount const zzz{};
EXPECT_EQ(zzz, beast::kZero);
// EXPECT_EQ(zzz, zz);
}
TEST(IOUAmountTest, sig_num)
{
IOUAmount const neg(-1, 0);
EXPECT_LT(neg.signum(), 0);
IOUAmount const zer(0, 0);
EXPECT_EQ(zer.signum(), 0);
IOUAmount const pos(1, 0);
EXPECT_GT(pos.signum(), 0);
}
TEST(IOUAmountTest, beast_zero)
{
using beast::kZero;
{
IOUAmount const z(kZero);
EXPECT_TRUE(z == kZero);
EXPECT_TRUE(z >= kZero);
EXPECT_TRUE(z <= kZero);
EXPECT_FALSE(z != kZero);
EXPECT_FALSE(z > kZero);
EXPECT_FALSE(z < kZero);
}
{
IOUAmount const neg(-2, 0);
EXPECT_TRUE(neg < kZero);
EXPECT_TRUE(neg <= kZero);
EXPECT_TRUE(neg != kZero);
EXPECT_FALSE(neg == kZero);
}
{
IOUAmount const pos(2, 0);
EXPECT_TRUE(pos > kZero);
EXPECT_TRUE(pos >= kZero);
EXPECT_TRUE(pos != kZero);
EXPECT_FALSE(pos == kZero);
}
}
TEST(IOUAmountTest, comparisons)
{
IOUAmount const n(-2, 0);
IOUAmount const z(0, 0);
IOUAmount const p(2, 0);
// For code readability, we want to use general
// EXPECT_TRUE instead of specific EXPECT_EQ etc.
EXPECT_TRUE(z == z);
EXPECT_TRUE(z >= z);
EXPECT_TRUE(z <= z);
EXPECT_TRUE(z == -z);
// NOLINTBEGIN(misc-redundant-expression)
EXPECT_FALSE(z > z);
EXPECT_FALSE(z < z);
EXPECT_FALSE(z != z);
// NOLINTEND(misc-redundant-expression)
EXPECT_FALSE(z != -z);
EXPECT_TRUE(n < z);
EXPECT_TRUE(n <= z);
EXPECT_TRUE(n != z);
EXPECT_FALSE(n > z);
EXPECT_FALSE(n >= z);
EXPECT_FALSE(n == z);
EXPECT_TRUE(p > z);
EXPECT_TRUE(p >= z);
EXPECT_TRUE(p != z);
EXPECT_FALSE(p < z);
EXPECT_FALSE(p <= z);
EXPECT_FALSE(p == z);
EXPECT_TRUE(n < p);
EXPECT_TRUE(n <= p);
EXPECT_TRUE(n != p);
EXPECT_FALSE(n > p);
EXPECT_FALSE(n >= p);
EXPECT_FALSE(n == p);
EXPECT_TRUE(p > n);
EXPECT_TRUE(p >= n);
EXPECT_TRUE(p != n);
EXPECT_FALSE(p < n);
EXPECT_FALSE(p <= n);
EXPECT_FALSE(p == n);
EXPECT_TRUE(p > -p);
EXPECT_TRUE(p >= -p);
EXPECT_TRUE(p != -p);
EXPECT_TRUE(n < -n);
EXPECT_TRUE(n <= -n);
EXPECT_TRUE(n != -n);
}
TEST(IOUAmountTest, to_string)
{
auto test = [](IOUAmount const& n, std::string const& expected) {
auto const result = to_string(n);
std::stringstream ss;
ss << "to_string(" << result << "). Expected: " << expected;
EXPECT_EQ(result, expected) << ss.str();
};
for (auto const mantissaSize : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const mg(mantissaSize);
test(IOUAmount(-2, 0), "-2");
test(IOUAmount(0, 0), "0");
test(IOUAmount(2, 0), "2");
test(IOUAmount(25, -3), "0.025");
test(IOUAmount(-25, -3), "-0.025");
test(IOUAmount(25, 1), "250");
test(IOUAmount(-25, 1), "-250");
test(IOUAmount(2, 20), "2e20");
test(IOUAmount(-2, -20), "-2e-20");
}
}
TEST(IOUAmountTest, mul_ratio)
{
/* The range for the mantissa when normalized */
constexpr std::int64_t kMinMantissa = 1000000000000000ull;
constexpr std::int64_t kMaxMantissa = 9999999999999999ull;
// log(2,maxMantissa) ~ 53.15
/* The range for the exponent when normalized */
constexpr int kMinExponent = -96;
constexpr int kMaxExponent = 80;
constexpr auto kMaxUInt = std::numeric_limits<std::uint32_t>::max();
{
// multiply by a number that would overflow the mantissa, then
// divide by the same number, and check we didn't lose any value
IOUAmount const bigMan(kMaxMantissa, 0);
EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, true));
// rounding mode shouldn't matter as the result is exact
EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, false));
}
{
// Similar test as above, but for negative values
IOUAmount const bigMan(-kMaxMantissa, 0);
EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, true));
// rounding mode shouldn't matter as the result is exact
EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, false));
}
{
// small amounts
IOUAmount const tiny(kMinMantissa, kMinExponent);
// Round up should give the smallest allowable number
EXPECT_EQ(tiny, mulRatio(tiny, 1, kMaxUInt, true));
EXPECT_EQ(tiny, mulRatio(tiny, kMaxUInt - 1, kMaxUInt, true));
// rounding down should be zero
EXPECT_EQ(beast::kZero, mulRatio(tiny, 1, kMaxUInt, false));
EXPECT_EQ(beast::kZero, mulRatio(tiny, kMaxUInt - 1, kMaxUInt, false));
// tiny negative numbers
IOUAmount const tinyNeg(-kMinMantissa, kMinExponent);
// Round up should give zero
EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, 1, kMaxUInt, true));
EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, true));
// rounding down should be tiny
EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, 1, kMaxUInt, false));
EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, false));
}
{ // rounding
{
IOUAmount const one(1, 0);
auto const rup = mulRatio(one, kMaxUInt - 1, kMaxUInt, true);
auto const rdown = mulRatio(one, kMaxUInt - 1, kMaxUInt, false);
EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1);
}
{
IOUAmount const big(kMaxMantissa, kMaxExponent);
auto const rup = mulRatio(big, kMaxUInt - 1, kMaxUInt, true);
auto const rdown = mulRatio(big, kMaxUInt - 1, kMaxUInt, false);
EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1);
}
{
IOUAmount const negOne(-1, 0);
auto const rup = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, true);
auto const rdown = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, false);
EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1);
}
}
{
// division by zero
IOUAmount const one(1, 0);
EXPECT_ANY_THROW({ mulRatio(one, 1, 0, true); });
}
{
// overflow
IOUAmount const big(kMaxMantissa, kMaxExponent);
EXPECT_ANY_THROW({ mulRatio(big, 2, 0, true); });
}
}
} // namespace xrpl

View File

@@ -0,0 +1,844 @@
#include <xrpl/basics/IntrusivePointer.h> // IWYU pragma: keep
#include <xrpl/basics/IntrusivePointer.ipp> // IWYU pragma: keep
#include <xrpl/basics/IntrusiveRefCounts.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono> // IWYU pragma: keep
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <latch>
#include <mutex>
#include <optional>
#include <random>
#include <stdexcept>
#include <thread>
#include <utility>
#include <variant>
#include <vector>
namespace xrpl::tests {
/*
* Experimentally, we discovered that using std::barrier performs extremely
* poorly (~1 hour vs ~1 minute to run the test suite) in certain macOS
* environments. To unblock our macOS CI pipeline, we replaced std::barrier with a
* custom mutex-based barrier (Barrier) that significantly improves performance
* without compromising correctness. For future reference, if we ever consider
* reintroducing std::barrier, the following configuration is known to exhibit the
* problem:
*
* Model Name: Mac mini
* Model Identifier: Mac14,3
* Model Number: Z16K000R4LL/A
* Chip: Apple M2
* Total Number of Cores: 8 (4 performance and 4 efficiency)
* Memory: 24 GB
* System Firmware Version: 11881.41.5
* OS Loader Version: 11881.1.1
* Apple clang version 16.0.0 (clang-1600.0.26.3)
* Target: arm64-apple-darwin24.0.0
* Thread model: posix
*
*/
struct Barrier
{
std::mutex mtx;
std::condition_variable cv;
int count;
int const initial;
std::size_t generation{0};
explicit Barrier(int n) : count(n), initial(n)
{
}
void
arriveAndWait()
{
std::unique_lock lock(mtx);
auto const currentGeneration = generation;
if (--count == 0)
{
++generation;
count = initial;
cv.notify_all();
}
else
{
cv.wait(lock, [&] { return generation != currentGeneration; });
}
}
};
namespace {
enum class TrackedState : std::uint8_t {
Uninitialized,
Alive,
PartiallyDeletedStarted,
PartiallyDeleted,
DeletedStarted,
Deleted
};
class TIBase : public IntrusiveRefCounts
{
public:
static constexpr std::size_t kMaxStates = 128;
static std::array<std::atomic<TrackedState>, kMaxStates> state;
static std::atomic<std::size_t> nextId;
static TrackedState
getState(std::size_t id)
{
if (id >= state.size())
throw std::out_of_range("TIBase state id out of range");
return state[id].load(std::memory_order_acquire);
}
static void
resetStates(bool resetCallback)
{
for (std::size_t i = 0; i < kMaxStates; ++i)
{
state[i].store(TrackedState::Uninitialized, std::memory_order_release);
}
nextId.store(0, std::memory_order_release);
if (resetCallback)
TIBase::tracingCallback = [](TrackedState, std::optional<TrackedState>) {};
}
struct ResetStatesGuard
{
bool resetCallback{false};
ResetStatesGuard(bool resetCallback) : resetCallback{resetCallback}
{
TIBase::resetStates(resetCallback);
}
~ResetStatesGuard()
{
TIBase::resetStates(resetCallback);
}
};
TIBase() : id{checkoutID()}
{
state[id].store(TrackedState::Alive, std::memory_order_relaxed);
}
~TIBase() override
{
using enum TrackedState;
tracingCallback(state[id].load(std::memory_order_relaxed), DeletedStarted);
// Use relaxed memory order to try to avoid atomic operations from
// adding additional memory synchronizations that may hide threading
// errors in the underlying shared pointer class.
state[id].store(DeletedStarted, std::memory_order_relaxed);
tracingCallback(DeletedStarted, Deleted);
state[id].store(TrackedState::Deleted, std::memory_order_relaxed);
tracingCallback(TrackedState::Deleted, std::nullopt);
}
void
partialDestructor() const
{
using enum TrackedState;
tracingCallback(state[id].load(std::memory_order_relaxed), PartiallyDeletedStarted);
state[id].store(PartiallyDeletedStarted, std::memory_order_relaxed);
tracingCallback(PartiallyDeletedStarted, PartiallyDeleted);
state[id].store(PartiallyDeleted, std::memory_order_relaxed);
tracingCallback(PartiallyDeleted, std::nullopt);
}
static std::function<void(TrackedState, std::optional<TrackedState>)> tracingCallback;
std::size_t const id;
private:
static std::size_t
checkoutID()
{
auto const id = nextId.fetch_add(1, std::memory_order_acq_rel);
if (id >= state.size())
throw std::out_of_range("TIBase state capacity exceeded");
return id;
}
};
std::array<std::atomic<TrackedState>, TIBase::kMaxStates> TIBase::state;
std::atomic<std::size_t> TIBase::nextId{0};
std::function<void(TrackedState, std::optional<TrackedState>)> TIBase::tracingCallback =
[](TrackedState, std::optional<TrackedState>) {};
} // namespace
TEST(IntrusiveSharedTest, basics)
{
{
TIBase::ResetStatesGuard const rsg{true};
TIBase const b;
EXPECT_EQ(b.useCount(), 1);
b.addWeakRef();
EXPECT_EQ(b.useCount(), 1);
auto s = b.releaseStrongRef();
EXPECT_EQ(s, ReleaseStrongRefAction::PartialDestroy);
EXPECT_EQ(b.useCount(), 0);
TIBase const* pb = &b;
partialDestructorFinished(&pb);
EXPECT_FALSE(pb);
auto w = b.releaseWeakRef();
EXPECT_EQ(w, ReleaseWeakRefAction::Destroy);
}
std::vector<SharedIntrusive<TIBase>> strong;
std::vector<WeakIntrusive<TIBase>> weak;
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
auto b = makeSharedIntrusive<TIBase>();
auto id = b->id;
EXPECT_EQ(TIBase::getState(id), Alive);
EXPECT_EQ(b->useCount(), 1);
for (int i = 0; i < 10; ++i)
{
strong.push_back(b);
}
b.reset();
EXPECT_EQ(TIBase::getState(id), Alive);
strong.resize(strong.size() - 1);
EXPECT_EQ(TIBase::getState(id), Alive);
strong.clear();
EXPECT_EQ(TIBase::getState(id), Deleted);
b = makeSharedIntrusive<TIBase>();
id = b->id;
EXPECT_EQ(TIBase::getState(id), Alive);
EXPECT_EQ(b->useCount(), 1);
for (int i = 0; i < 10; ++i)
{
weak.emplace_back(b);
EXPECT_EQ(b->useCount(), 1);
}
EXPECT_EQ(TIBase::getState(id), Alive);
weak.resize(weak.size() - 1);
EXPECT_EQ(TIBase::getState(id), Alive);
b.reset();
EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
while (!weak.empty())
{
weak.resize(weak.size() - 1);
if (!weak.empty())
{
EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
}
}
EXPECT_EQ(TIBase::getState(id), Deleted);
}
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
auto b = makeSharedIntrusive<TIBase>();
auto id = b->id;
EXPECT_EQ(TIBase::getState(id), Alive);
WeakIntrusive<TIBase> w{b};
EXPECT_EQ(TIBase::getState(id), Alive);
auto s = w.lock();
EXPECT_TRUE(s && s->useCount() == 2);
b.reset();
EXPECT_TRUE(TIBase::getState(id) == Alive);
EXPECT_TRUE(s && s->useCount() == 1);
s.reset();
EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
EXPECT_TRUE(w.expired());
s = w.lock();
// Cannot convert a weak pointer to a strong pointer if object is
// already partially deleted
EXPECT_FALSE(s);
w.reset();
EXPECT_EQ(TIBase::getState(id), Deleted);
}
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
using swu = SharedWeakUnion<TIBase>;
swu b = makeSharedIntrusive<TIBase>();
EXPECT_TRUE(b.isStrong() && b.useCount() == 1);
auto id = b.get()->id;
EXPECT_EQ(TIBase::getState(id), Alive);
swu w = b;
EXPECT_TRUE(TIBase::getState(id) == Alive);
EXPECT_TRUE(w.isStrong() && b.useCount() == 2);
w.convertToWeak();
EXPECT_TRUE(w.isWeak() && b.useCount() == 1);
swu s = w;
EXPECT_TRUE(s.isWeak() && b.useCount() == 1);
s.convertToStrong();
EXPECT_TRUE(s.isStrong() && b.useCount() == 2);
b.reset();
EXPECT_EQ(TIBase::getState(id), Alive);
EXPECT_EQ(s.useCount(), 1);
EXPECT_FALSE(w.expired());
s.reset();
EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
EXPECT_TRUE(w.expired());
w.convertToStrong();
// Cannot convert a weak pointer to a strong pointer if object is
// already partially deleted
EXPECT_TRUE(w.isWeak());
w.reset();
EXPECT_EQ(TIBase::getState(id), Deleted);
}
{
// Testing SharedWeakUnion assignment operator
TIBase::ResetStatesGuard const rsg{true};
auto strong1 = makeSharedIntrusive<TIBase>();
auto strong2 = makeSharedIntrusive<TIBase>();
auto id1 = strong1->id;
auto id2 = strong2->id;
EXPECT_NE(id1, id2);
SharedWeakUnion<TIBase> union1 = strong1;
SharedWeakUnion<TIBase> union2 = strong2;
EXPECT_TRUE(union1.isStrong());
EXPECT_TRUE(union2.isStrong());
EXPECT_EQ(union1.get(), strong1.get());
EXPECT_EQ(union2.get(), strong2.get());
// 1) Normal assignment: explicitly calls SharedWeakUnion assignment
union1 = union2;
EXPECT_TRUE(union1.isStrong());
EXPECT_TRUE(union2.isStrong());
EXPECT_EQ(union1.get(), union2.get());
EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive);
EXPECT_EQ(TIBase::getState(id2), TrackedState::Alive);
// 2) Test self-assignment
EXPECT_TRUE(union1.isStrong());
EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive);
int const initialRefCount = strong1->useCount();
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wself-assign-overloaded"
union1 = union1; // Self-assignment
#pragma clang diagnostic pop
EXPECT_TRUE(union1.isStrong());
EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive);
EXPECT_EQ(strong1->useCount(), initialRefCount);
// 3) Test assignment from null union pointer
union1 = SharedWeakUnion<TIBase>();
EXPECT_EQ(union1.get(), nullptr);
// 4) Test assignment to expired union pointer
strong2.reset();
union2.reset();
union1 = union2;
EXPECT_EQ(union1.get(), nullptr);
EXPECT_EQ(TIBase::getState(id2), TrackedState::Deleted);
}
}
TEST(IntrusiveSharedTest, partial_delete)
{
// This test creates two threads. One with a strong pointer and one
// with a weak pointer. The strong pointer is reset while the weak
// pointer still holds a reference, triggering a partial delete.
// While the partial delete function runs (a sleep is inserted) the
// weak pointer is reset. The destructor should wait to run until
// after the partial delete function has completed running.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
auto strong = makeSharedIntrusive<TIBase>();
WeakIntrusive<TIBase> weak{strong};
std::atomic<bool> destructorRan{false};
std::atomic<bool> partialDeleteRan{false};
std::latch partialDeleteStartedSyncPoint{2};
strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
if (next == DeletedStarted)
{
// strong goes out of scope while weak is still in scope
// This checks that partialDelete has run to completion
// before the destructor is called. A sleep is inserted
// inside the partial delete to make sure the destructor is
// given an opportunity to run during partial delete.
EXPECT_EQ(cur, PartiallyDeleted);
}
if (next == PartiallyDeletedStarted)
{
partialDeleteStartedSyncPoint.arrive_and_wait();
using namespace std::chrono_literals;
// Sleep and let the weak pointer go out of scope,
// potentially triggering a destructor while partial delete
// is running. The test is to make sure that doesn't happen.
std::this_thread::sleep_for(800ms);
}
if (next == PartiallyDeleted)
{
EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load());
}
if (next == Deleted)
{
EXPECT_FALSE(destructorRan.exchange(true));
}
};
std::thread t1{[&] {
partialDeleteStartedSyncPoint.arrive_and_wait();
weak.reset(); // Trigger a full delete as soon as the partial
// delete starts
}};
std::thread t2{[&] {
strong.reset(); // Trigger a partial delete
}};
t1.join();
t2.join();
EXPECT_TRUE(destructorRan.load() && partialDeleteRan.load());
}
TEST(IntrusiveSharedTest, destructor)
{
// This test creates two threads. One with a strong pointer and one
// with a weak pointer. The weak pointer is reset while the strong
// pointer still holds a reference. Then the strong pointer is
// reset. Only the destructor should run. The partial destructor
// should not be called. Since the weak reset runs to completion
// before the strong pointer is reset, threading doesn't add much to
// this test, but there is no harm in keeping it.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
auto strong = makeSharedIntrusive<TIBase>();
WeakIntrusive<TIBase> weak{strong};
std::atomic<bool> destructorRan{false};
std::atomic<bool> partialDeleteRan{false};
std::latch weakResetSyncPoint{2};
strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
if (next == PartiallyDeleted)
{
EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load());
}
if (next == Deleted)
{
EXPECT_FALSE(destructorRan.exchange(true));
}
};
std::thread t1{[&] {
weak.reset();
weakResetSyncPoint.arrive_and_wait();
}};
std::thread t2{[&] {
weakResetSyncPoint.arrive_and_wait();
strong.reset(); // Trigger a partial delete
}};
t1.join();
t2.join();
EXPECT_TRUE(destructorRan.load() && !partialDeleteRan.load());
}
TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant)
{
// This test creates and destroys many strong and weak pointers in a
// loop. There is a random mix of strong and weak pointers stored in
// a vector (held as a variant). Both threads clear all the pointers
// and check that the invariants hold.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
std::atomic<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
int const s = destructionState.load(std::memory_order_relaxed);
return {(s & 1) != 0, (s & 2) != 0};
};
auto setDestructorRan = [&]() -> void {
destructionState.fetch_or(1, std::memory_order_acq_rel);
};
auto setPartialDeleteRan = [&]() -> void {
destructionState.fetch_or(2, std::memory_order_acq_rel);
};
auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
auto [destructorRan, partialDeleteRan] = getDestructorState();
if (next == PartiallyDeleted)
{
EXPECT_FALSE(partialDeleteRan || destructorRan);
setPartialDeleteRan();
}
if (next == Deleted)
{
EXPECT_FALSE(destructorRan);
setDestructorRan();
}
};
auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng)
-> std::vector<std::variant<SharedIntrusive<TIBase>, WeakIntrusive<TIBase>>> {
std::vector<std::variant<SharedIntrusive<TIBase>, WeakIntrusive<TIBase>>> result;
std::uniform_int_distribution<> toCreateDist(4, 64);
std::uniform_int_distribution<> isStrongDist(0, 1);
auto numToCreate = toCreateDist(eng);
result.reserve(numToCreate);
for (int i = 0; i < numToCreate; ++i)
{
if (isStrongDist(eng))
{
result.emplace_back(SharedIntrusive<TIBase>(toClone));
}
else
{
result.emplace_back(WeakIntrusive<TIBase>(toClone));
}
}
return result;
};
constexpr int kLoopIters = 2 * 1024;
constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toClone;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToCloneSyncPoint{kNumThreads};
Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
auto engines = [&]() -> std::vector<std::default_random_engine> {
std::random_device rd;
std::vector<std::default_random_engine> result;
result.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
result.emplace_back(rd());
return result;
}();
// cloneAndDestroy clones the strong pointer into a vector of mixed
// strong and weak pointers and destroys them all at once.
// threadId==0 is special.
auto cloneAndDestroy = [&](int threadId) {
for (int i = 0; i < kLoopIters; ++i)
{
// ------ Sync Point ------
loopStartSyncPoint.arriveAndWait();
// only thread 0 should reset the state
std::optional<TIBase::ResetStatesGuard> rsg;
if (threadId == 0)
{
// Thread 0 is the genesis thread. It creates the strong
// pointers to be cloned by the other threads. This
// thread will also check that the destructor ran and
// clear the temporary variables.
rsg.emplace(false);
auto [destructorRan, partialDeleteRan] = getDestructorState();
EXPECT_TRUE(i == 0 || destructorRan);
destructionState.store(0, std::memory_order_release);
toClone.clear();
toClone.resize(kNumThreads);
auto strong = makeSharedIntrusive<TIBase>();
strong->tracingCallback = tracingCallback;
std::ranges::fill(toClone, strong);
}
// ------ Sync Point ------
postCreateToCloneSyncPoint.arriveAndWait();
auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
toClone[threadId].reset();
// ------ Sync Point ------
postCreateVecOfPointersSyncPoint.arriveAndWait();
v.clear();
}
};
std::vector<std::thread> threads;
threads.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
{
threads.emplace_back(cloneAndDestroy, i);
}
for (int i = 0; i < kNumThreads; ++i)
{
threads[i].join();
}
}
TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union)
{
// This test creates and destroys many SharedWeak pointers in a
// loop. All the pointers start as strong and a loop randomly
// convert them between strong and weak pointers. Both threads clear
// all the pointers and check that the invariants hold.
//
// Note: This test also differs from the test above in that the pointers
// randomly change from strong to weak and from weak to strong in a
// loop. This can't be done in the variant test above because variant is
// not thread safe while the SharedWeakUnion is thread safe.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
std::atomic<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
int const s = destructionState.load(std::memory_order_relaxed);
return {(s & 1) != 0, (s & 2) != 0};
};
auto setDestructorRan = [&]() -> void {
destructionState.fetch_or(1, std::memory_order_acq_rel);
};
auto setPartialDeleteRan = [&]() -> void {
destructionState.fetch_or(2, std::memory_order_acq_rel);
};
auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
auto [destructorRan, partialDeleteRan] = getDestructorState();
if (next == PartiallyDeleted)
{
EXPECT_FALSE(partialDeleteRan || destructorRan);
setPartialDeleteRan();
}
if (next == Deleted)
{
EXPECT_FALSE(destructorRan);
setDestructorRan();
}
};
auto createVecOfPointers =
[&](auto const& toClone,
std::default_random_engine& eng) -> std::vector<SharedWeakUnion<TIBase>> {
std::vector<SharedWeakUnion<TIBase>> result;
std::uniform_int_distribution<> toCreateDist(4, 64);
auto numToCreate = toCreateDist(eng);
result.reserve(numToCreate);
for (int i = 0; i < numToCreate; ++i)
result.emplace_back(SharedIntrusive<TIBase>(toClone));
return result;
};
constexpr int kLoopIters = 2 * 1024;
constexpr int kFlipPointersLoopIters = 256;
constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toClone;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToCloneSyncPoint{kNumThreads};
Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
Barrier postFlipPointersLoopSyncPoint{kNumThreads};
auto engines = [&]() -> std::vector<std::default_random_engine> {
std::random_device rd;
std::vector<std::default_random_engine> result;
result.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
result.emplace_back(rd());
return result;
}();
// cloneAndDestroy clones the strong pointer into a vector of
// mixed strong and weak pointers, runs a loop that randomly
// changes strong pointers to weak pointers, and destroys them
// all at once.
auto cloneAndDestroy = [&](int threadId) {
for (int i = 0; i < kLoopIters; ++i)
{
// ------ Sync Point ------
loopStartSyncPoint.arriveAndWait();
// only thread 0 should reset the state
std::optional<TIBase::ResetStatesGuard> rsg;
if (threadId == 0)
{
// threadId 0 is the genesis thread. It creates the
// strong point to be cloned by the other threads. This
// thread will also check that the destructor ran and
// clear the temporary variables.
rsg.emplace(false);
auto [destructorRan, partialDeleteRan] = getDestructorState();
EXPECT_TRUE(i == 0 || destructorRan);
destructionState.store(0, std::memory_order_release);
toClone.clear();
toClone.resize(kNumThreads);
auto strong = makeSharedIntrusive<TIBase>();
strong->tracingCallback = tracingCallback;
std::ranges::fill(toClone, strong);
}
// ------ Sync Point ------
postCreateToCloneSyncPoint.arriveAndWait();
auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
toClone[threadId].reset();
// ------ Sync Point ------
postCreateVecOfPointersSyncPoint.arriveAndWait();
std::uniform_int_distribution<> isStrongDist(0, 1);
for (int f = 0; f < kFlipPointersLoopIters; ++f)
{
for (auto& p : v)
{
if (isStrongDist(engines[threadId]))
{
p.convertToStrong();
}
else
{
p.convertToWeak();
}
}
}
// ------ Sync Point ------
postFlipPointersLoopSyncPoint.arriveAndWait();
v.clear();
}
};
std::vector<std::thread> threads;
threads.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
{
threads.emplace_back(cloneAndDestroy, i);
}
for (int i = 0; i < kNumThreads; ++i)
{
threads[i].join();
}
}
TEST(IntrusiveSharedTest, multithreaded_locking_weak)
{
// This test creates a single shared atomic pointer that multiple thread
// create weak pointers from. The threads then lock the weak pointers.
// Both threads clear all the pointers and check that the invariants
// hold.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
std::atomic<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
int const s = destructionState.load(std::memory_order_relaxed);
return {(s & 1) != 0, (s & 2) != 0};
};
auto setDestructorRan = [&]() -> void {
destructionState.fetch_or(1, std::memory_order_acq_rel);
};
auto setPartialDeleteRan = [&]() -> void {
destructionState.fetch_or(2, std::memory_order_acq_rel);
};
auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
auto [destructorRan, partialDeleteRan] = getDestructorState();
if (next == PartiallyDeleted)
{
EXPECT_FALSE(partialDeleteRan || destructorRan);
setPartialDeleteRan();
}
if (next == Deleted)
{
EXPECT_FALSE(destructorRan);
setDestructorRan();
}
};
constexpr int kLoopIters = 2 * 1024;
constexpr int kLockWeakLoopIters = 256;
constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toLock;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToLockSyncPoint{kNumThreads};
Barrier postLockWeakLoopSyncPoint{kNumThreads};
// lockAndDestroy creates weak pointers from the strong pointer
// and runs a loop that locks the weak pointer. At the end of the loop
// all the pointers are destroyed all at once.
auto lockAndDestroy = [&](int threadId) {
for (int i = 0; i < kLoopIters; ++i)
{
// ------ Sync Point ------
loopStartSyncPoint.arriveAndWait();
// only thread 0 should reset the state
std::optional<TIBase::ResetStatesGuard> rsg;
if (threadId == 0)
{
// threadId 0 is the genesis thread. It creates the
// strong point to be locked by the other threads. This
// thread will also check that the destructor ran and
// clear the temporary variables.
rsg.emplace(false);
auto [destructorRan, partialDeleteRan] = getDestructorState();
EXPECT_TRUE(i == 0 || destructorRan);
destructionState.store(0, std::memory_order_release);
toLock.clear();
toLock.resize(kNumThreads);
auto strong = makeSharedIntrusive<TIBase>();
strong->tracingCallback = tracingCallback;
std::ranges::fill(toLock, strong);
}
// ------ Sync Point ------
postCreateToLockSyncPoint.arriveAndWait();
// Multiple threads all create a weak pointer from the same
// strong pointer
WeakIntrusive const weak{toLock[threadId]};
for (int wi = 0; wi < kLockWeakLoopIters; ++wi)
{
EXPECT_FALSE(weak.expired());
auto strong = weak.lock();
EXPECT_TRUE(strong);
}
// ------ Sync Point ------
postLockWeakLoopSyncPoint.arriveAndWait();
toLock[threadId].reset();
}
};
std::vector<std::thread> threads;
threads.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
{
threads.emplace_back(lockAndDestroy, i);
}
for (int i = 0; i < kNumThreads; ++i)
{
threads[i].join();
}
}
} // namespace xrpl::tests

View File

@@ -0,0 +1,81 @@
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Protocol.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <string>
namespace xrpl {
class KeyCacheTest : public ::testing::Test
{
public:
};
TEST_F(KeyCacheTest, key_cache)
{
using namespace std::chrono_literals;
TestStopwatch clock;
clock.set(0);
using Key = std::string;
using Cache = TaggedCache<Key, int, true>;
beast::Journal const j{TestSink::instance()};
// Insert an item, retrieve it, and age it so it gets purged.
{
Cache c("test", LedgerIndex(1), 2s, clock, j);
EXPECT_EQ(c.size(), 0);
EXPECT_TRUE(c.insert("one"));
EXPECT_FALSE(c.insert("one"));
EXPECT_EQ(c.size(), 1);
EXPECT_TRUE(c.touchIfExists("one"));
++clock;
c.sweep();
EXPECT_EQ(c.size(), 1);
++clock;
c.sweep();
EXPECT_EQ(c.size(), 0);
EXPECT_FALSE(c.touchIfExists("one"));
}
// Insert two items, have one expire
{
Cache c("test", LedgerIndex(2), 2s, clock, j);
EXPECT_TRUE(c.insert("one"));
EXPECT_EQ(c.size(), 1);
EXPECT_TRUE(c.insert("two"));
EXPECT_EQ(c.size(), 2);
++clock;
c.sweep();
EXPECT_EQ(c.size(), 2);
EXPECT_TRUE(c.touchIfExists("two"));
++clock;
c.sweep();
EXPECT_EQ(c.size(), 1);
}
// Insert three items (1 over limit), sweep
{
Cache c("test", LedgerIndex(2), 3s, clock, j);
EXPECT_TRUE(c.insert("one"));
++clock;
EXPECT_TRUE(c.insert("two"));
++clock;
EXPECT_TRUE(c.insert("three"));
++clock;
EXPECT_EQ(c.size(), 3);
c.sweep();
EXPECT_LT(c.size(), 3);
}
}
} // namespace xrpl

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,293 @@
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/ToString.h>
#include <gtest/gtest.h>
#include <string>
namespace xrpl {
class StringUtilitiesTest : public ::testing::Test
{
public:
static void
testUnHexSuccess(std::string const& strIn, std::string const& strExpected)
{
auto rv = strUnHex(strIn);
EXPECT_TRUE(rv);
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(makeSlice(*rv), makeSlice(strExpected));
}
static void
testUnHexFailure(std::string const& strIn)
{
auto rv = strUnHex(strIn);
EXPECT_FALSE(rv);
}
};
TEST_F(StringUtilitiesTest, un_hex)
{
testUnHexSuccess("526970706c6544", "RippleD");
testUnHexSuccess("A", "\n");
testUnHexSuccess("0A", "\n");
testUnHexSuccess("D0A", "\r\n");
testUnHexSuccess("0D0A", "\r\n");
testUnHexSuccess("200D0A", " \r\n");
testUnHexSuccess("282A2B2C2D2E2F29", "(*+,-./)");
// Check for things which contain some or only invalid characters
testUnHexFailure("123X");
testUnHexFailure("V");
testUnHexFailure("XRP");
}
TEST_F(StringUtilitiesTest, parse_url)
{
// Expected passes.
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_TRUE(pUrl.domain.empty());
EXPECT_FALSE(pUrl.port);
// RFC 3986:
// > In general, a URI that uses the generic syntax for authority
// with an empty path should be normalized to a path of "/".
// Do we want to normalize paths?
EXPECT_TRUE(pUrl.path.empty());
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme:///"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_TRUE(pUrl.domain.empty());
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "lower://domain"));
EXPECT_EQ(pUrl.scheme, "lower");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_TRUE(pUrl.path.empty());
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "UPPER://domain:234/"));
EXPECT_EQ(pUrl.scheme, "upper");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_EQ(*pUrl.port, 234); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "Mixed://domain/path"));
EXPECT_EQ(pUrl.scheme, "mixed");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/path");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://[::1]:123/path"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "::1");
EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/path");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain:123/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_EQ(pUrl.username, "user");
EXPECT_EQ(pUrl.password, "pass");
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://user@domain:123/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_EQ(pUrl.username, "user");
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://:pass@domain:123/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_EQ(pUrl.password, "pass");
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://domain:123/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_EQ(pUrl.username, "user");
EXPECT_EQ(pUrl.password, "pass");
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://user@domain/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_EQ(pUrl.username, "user");
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://:pass@domain/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_EQ(pUrl.password, "pass");
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://domain/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme:///path/to/file"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_TRUE(pUrl.domain.empty());
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/path/to/file");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain/path/with/an@sign"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_EQ(pUrl.username, "user");
EXPECT_EQ(pUrl.password, "pass");
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/path/with/an@sign");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://domain/path/with/an@sign"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/path/with/an@sign");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://:999/"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, ":999");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "http://::1:1234/validators"));
EXPECT_EQ(pUrl.scheme, "http");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "::0.1.18.52");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/validators");
}
// Expected fails.
{
ParsedUrl pUrl;
EXPECT_FALSE(parseUrl(pUrl, ""));
EXPECT_FALSE(parseUrl(pUrl, "nonsense"));
EXPECT_FALSE(parseUrl(pUrl, "://"));
EXPECT_FALSE(parseUrl(pUrl, ":///"));
EXPECT_FALSE(parseUrl(pUrl, "scheme://user:pass@domain:65536/abc:321"));
EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:23498765/"));
EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:0/"));
EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:+7/"));
EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:-7234/"));
EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:@#$56!/"));
}
{
std::string const strUrl("s://" + std::string(8192, ':'));
ParsedUrl pUrl;
EXPECT_FALSE(parseUrl(pUrl, strUrl));
}
}
TEST_F(StringUtilitiesTest, to_string)
{
auto result = to_string("hello");
EXPECT_EQ(result, "hello");
}
} // namespace xrpl

View File

@@ -0,0 +1,246 @@
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/IntrusivePointer.h>
#include <xrpl/basics/IntrusiveRefCounts.h>
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Protocol.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <memory>
#include <string>
#include <utility>
namespace xrpl {
/*
I guess you can put some items in, make sure they're still there. Let some
time pass, make sure they're gone. Keep a strong pointer to one of them, make
sure you can still find it even after time passes. Create two objects with
the same key, canonicalize them both and make sure you get the same object.
Put an object in but keep a strong pointer to it, advance the clock a lot,
then canonicalize a new object with the same key, make sure you get the
original object.
*/
TEST(TaggedCacheTest, tagged_cache)
{
using namespace std::chrono_literals;
beast::Journal const journal{TestSink::instance()};
TestStopwatch clock;
clock.set(0);
using Key = LedgerIndex;
using Value = std::string;
using Cache = TaggedCache<Key, Value>;
Cache c("test", 1, 1s, clock, journal);
// Insert an item, retrieve it, and age it so it gets purged.
{
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 0);
EXPECT_FALSE(c.insert(1, "one"));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.getTrackSize(), 1);
{
std::string s;
EXPECT_TRUE(c.retrieve(1, s));
EXPECT_EQ(s, "one");
}
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 0);
}
// Insert an item, maintain a strong pointer, age it, and
// verify that the entry still exists.
{
EXPECT_FALSE(c.insert(2, "two"));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.getTrackSize(), 1);
{
auto p = c.fetch(2);
EXPECT_NE(p, nullptr);
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 1);
}
// Make sure its gone now that our reference is gone
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 0);
}
// Insert the same key/value pair and make sure we get the same result
{
EXPECT_FALSE(c.insert(3, "three"));
{
auto const p1 = c.fetch(3);
auto p2 = std::make_shared<Value>("three");
c.canonicalizeReplaceClient(3, p2);
EXPECT_EQ(p1.get(), p2.get());
}
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 0);
}
// Put an object in but keep a strong pointer to it, advance the clock a
// lot, then canonicalize a new object with the same key, make sure you
// get the original object.
{
// Put an object in
EXPECT_FALSE(c.insert(4, "four"));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.getTrackSize(), 1);
{
// Keep a strong pointer to it
auto const p1 = c.fetch(4);
EXPECT_NE(p1, nullptr);
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.getTrackSize(), 1);
// Advance the clock a lot
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 1);
// Canonicalize a new object with the same key
auto p2 = std::make_shared<std::string>("four");
EXPECT_TRUE(c.canonicalizeReplaceClient(4, p2));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.getTrackSize(), 1);
// Make sure we get the original object
EXPECT_EQ(p1.get(), p2.get());
}
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 0);
}
{
EXPECT_FALSE(c.insert(5, "five"));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.size(), 1);
{
auto const p1 = c.fetch(5);
EXPECT_NE(p1, nullptr);
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.size(), 1);
// Advance the clock a lot
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.size(), 1);
auto p2 = std::make_shared<std::string>("five_2");
EXPECT_TRUE(c.canonicalizeReplaceCache(5, p2));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.size(), 1);
// Make sure the caller's original pointer is unchanged
EXPECT_NE(p1.get(), p2.get());
EXPECT_EQ(*p2, "five_2");
auto const p3 = c.fetch(5);
EXPECT_NE(p3, nullptr);
EXPECT_EQ(p3.get(), p2.get());
EXPECT_NE(p3.get(), p1.get());
}
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.size(), 0);
}
{
struct MyRefCountObject : IntrusiveRefCounts
{
std::string data;
// Needed to support weak intrusive pointers
virtual void
partialDestructor()
{
}
MyRefCountObject() = default;
explicit MyRefCountObject(std::string data) : data(std::move(data))
{
}
bool
operator==(std::string const& other) const
{
return data == other;
}
};
using IntrPtrCache = TaggedCache<
Key,
MyRefCountObject,
/*IsKeyCache*/ false,
intr_ptr::SharedWeakUnionPtr<MyRefCountObject>,
intr_ptr::SharedPtr<MyRefCountObject>>;
IntrPtrCache intrPtrCache("IntrPtrTest", 1, 1s, clock, journal);
intrPtrCache.canonicalizeReplaceCache(1, intr_ptr::makeShared<MyRefCountObject>("one"));
EXPECT_EQ(intrPtrCache.getCacheSize(), 1);
EXPECT_EQ(intrPtrCache.size(), 1);
{
{
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("one_replaced"));
auto p = intrPtrCache.fetch(1);
EXPECT_EQ(*p, "one_replaced");
// Advance the clock a lot
++clock;
intrPtrCache.sweep();
EXPECT_EQ(intrPtrCache.getCacheSize(), 0);
EXPECT_EQ(intrPtrCache.size(), 1);
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("one_replaced_2"));
auto p2 = intrPtrCache.fetch(1);
EXPECT_EQ(*p2, "one_replaced_2");
intrPtrCache.del(1, true);
}
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("one_replaced_3"));
auto p3 = intrPtrCache.fetch(1);
EXPECT_EQ(*p3, "one_replaced_3");
}
++clock;
intrPtrCache.sweep();
EXPECT_EQ(intrPtrCache.getCacheSize(), 0);
EXPECT_EQ(intrPtrCache.size(), 0);
}
}
} // namespace xrpl

View File

@@ -0,0 +1,328 @@
#include <xrpl/protocol/Units.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/XRPAmount.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
#include <type_traits>
namespace xrpl::test {
TEST(UnitsTest, types)
{
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
XRPAmount const x{100};
EXPECT_EQ(x.drops(), 100);
EXPECT_TRUE((std::is_same_v<decltype(x)::unit_type, unit::dropTag>));
auto y = 4u * x;
EXPECT_EQ(y.value(), 400);
EXPECT_TRUE((std::is_same_v<decltype(y)::unit_type, unit::dropTag>));
auto z = 4 * y;
EXPECT_EQ(z.value(), 1600);
EXPECT_TRUE((std::is_same_v<decltype(z)::unit_type, unit::dropTag>));
FeeLevel32 const f{10};
FeeLevel32 const baseFee{100};
auto drops = mulDiv(baseFee, x, f);
EXPECT_TRUE(drops);
EXPECT_EQ(drops.value(), 1000); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_TRUE(
(std::is_same_v<std::remove_reference_t<decltype(*drops)>::unit_type, unit::dropTag>));
EXPECT_TRUE((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
{
XRPAmount const x{100};
EXPECT_EQ(x.value(), 100);
EXPECT_TRUE((std::is_same_v<decltype(x)::unit_type, unit::dropTag>));
auto y = 4u * x;
EXPECT_EQ(y.value(), 400);
EXPECT_TRUE((std::is_same_v<decltype(y)::unit_type, unit::dropTag>));
FeeLevel64 const f{10};
FeeLevel64 const baseFee{100};
auto drops = mulDiv(baseFee, x, f);
EXPECT_TRUE(drops);
EXPECT_EQ(drops.value(), 1000); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_TRUE(
(std::is_same_v<std::remove_reference_t<decltype(*drops)>::unit_type, unit::dropTag>));
EXPECT_TRUE((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
{
FeeLevel64 const x{1024};
EXPECT_EQ(x.value(), 1024);
EXPECT_TRUE((std::is_same_v<decltype(x)::unit_type, unit::feelevelTag>));
std::uint64_t const m = 4;
auto y = m * x;
EXPECT_EQ(y.value(), 4096);
EXPECT_TRUE((std::is_same_v<decltype(y)::unit_type, unit::feelevelTag>));
XRPAmount const basefee{10};
FeeLevel64 const referencefee{256};
auto drops = mulDiv(x, basefee, referencefee);
EXPECT_TRUE(drops);
EXPECT_EQ(drops.value(), 40); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_TRUE(
(std::is_same_v<std::remove_reference_t<decltype(*drops)>::unit_type, unit::dropTag>));
EXPECT_TRUE((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
}
TEST(UnitsTest, json)
{
// Json value functionality
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
FeeLevel32 const x{std::numeric_limits<std::uint32_t>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::UInt);
EXPECT_EQ(y, json::Value{x.fee()});
}
{
FeeLevel32 const x{std::numeric_limits<std::uint32_t>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::UInt);
EXPECT_EQ(y, json::Value{x.fee()});
}
{
FeeLevel64 const x{std::numeric_limits<std::uint64_t>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::UInt);
EXPECT_EQ(y, json::Value{std::numeric_limits<std::uint32_t>::max()});
}
{
FeeLevel64 const x{std::numeric_limits<std::uint64_t>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::UInt);
EXPECT_EQ(y, json::Value{0});
}
{
FeeLevelDouble const x{std::numeric_limits<double>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Real);
EXPECT_EQ(y, json::Value{std::numeric_limits<double>::max()});
}
{
FeeLevelDouble const x{std::numeric_limits<double>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Real);
EXPECT_EQ(y, json::Value{std::numeric_limits<double>::min()});
}
{
XRPAmount const x{std::numeric_limits<std::int64_t>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Int);
EXPECT_EQ(y, json::Value{std::numeric_limits<std::int32_t>::max()});
}
{
XRPAmount const x{std::numeric_limits<std::int64_t>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Int);
EXPECT_EQ(y, json::Value{std::numeric_limits<std::int32_t>::min()});
}
}
TEST(UnitsTest, functions)
{
// Explicitly test every defined function for the ValueUnit class
// since some of them are templated, but not used anywhere else.
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
auto make = [&](auto x) -> FeeLevel64 { return x; };
auto explicitmake = [&](auto x) -> FeeLevel64 { return FeeLevel64{x}; };
[[maybe_unused]]
FeeLevel64 const defaulted{};
FeeLevel64 test{0};
EXPECT_EQ(test.fee(), 0);
test = explicitmake(beast::kZero);
EXPECT_EQ(test.fee(), 0);
test = beast::kZero;
EXPECT_EQ(test.fee(), 0);
test = explicitmake(100u);
EXPECT_EQ(test.fee(), 100);
FeeLevel64 const targetSame{200u};
FeeLevel32 const targetOther{300u};
test = make(targetSame);
EXPECT_EQ(test.fee(), 200);
EXPECT_EQ(test, targetSame);
EXPECT_TRUE(test < FeeLevel64{1000});
EXPECT_TRUE(test > FeeLevel64{100});
test = make(targetOther);
EXPECT_EQ(test.fee(), 300);
EXPECT_EQ(test, targetOther);
test = std::uint64_t(200);
EXPECT_EQ(test.fee(), 200);
test = std::uint32_t(300);
EXPECT_EQ(test.fee(), 300);
test = targetSame;
EXPECT_EQ(test.fee(), 200);
test = targetOther.fee();
EXPECT_EQ(test.fee(), 300);
EXPECT_EQ(test, targetOther);
test = targetSame * 2;
EXPECT_EQ(test.fee(), 400);
test = 3 * targetSame;
EXPECT_EQ(test.fee(), 600);
test = targetSame / 10;
EXPECT_EQ(test.fee(), 20);
test += targetSame;
EXPECT_EQ(test.fee(), 220);
test -= targetSame;
EXPECT_EQ(test.fee(), 20);
test++;
EXPECT_EQ(test.fee(), 21);
++test;
EXPECT_EQ(test.fee(), 22);
test--;
EXPECT_EQ(test.fee(), 21);
--test;
EXPECT_EQ(test.fee(), 20);
test *= 5;
EXPECT_EQ(test.fee(), 100);
test /= 2;
EXPECT_EQ(test.fee(), 50);
test %= 13;
EXPECT_EQ(test.fee(), 11);
/*
// illegal with unsigned
test = -test;
EXPECT_EQ(test.fee(), -11);
EXPECT_EQ(test.signum(), -1);
EXPECT_EQ(to_string(test), "-11");
*/
EXPECT_TRUE(test);
test = 0;
EXPECT_FALSE(test);
EXPECT_EQ(test.signum(), 0);
test = targetSame;
EXPECT_EQ(test.signum(), 1);
EXPECT_EQ(to_string(test), "200");
}
{
auto make = [&](auto x) -> FeeLevelDouble { return x; };
auto explicitmake = [&](auto x) -> FeeLevelDouble { return FeeLevelDouble{x}; };
[[maybe_unused]]
FeeLevelDouble const defaulted{};
FeeLevelDouble test{0};
EXPECT_EQ(test.fee(), 0);
test = explicitmake(beast::kZero);
EXPECT_EQ(test.fee(), 0);
test = beast::kZero;
EXPECT_EQ(test.fee(), 0);
test = explicitmake(100.0);
EXPECT_EQ(test.fee(), 100);
FeeLevelDouble const targetSame{200.0};
FeeLevel64 const targetOther{300};
test = make(targetSame);
EXPECT_EQ(test.fee(), 200);
EXPECT_EQ(test, targetSame);
EXPECT_TRUE(test < FeeLevelDouble{1000.0});
EXPECT_TRUE(test > FeeLevelDouble{100.0});
test = targetOther.fee();
EXPECT_EQ(test.fee(), 300);
EXPECT_EQ(test, targetOther);
test = 200.0;
EXPECT_EQ(test.fee(), 200);
test = std::uint64_t(300);
EXPECT_EQ(test.fee(), 300);
test = targetSame;
EXPECT_EQ(test.fee(), 200);
test = targetSame * 2;
EXPECT_EQ(test.fee(), 400);
test = 3 * targetSame;
EXPECT_EQ(test.fee(), 600);
test = targetSame / 10;
EXPECT_EQ(test.fee(), 20);
test += targetSame;
EXPECT_EQ(test.fee(), 220);
test -= targetSame;
EXPECT_EQ(test.fee(), 20);
test++;
EXPECT_EQ(test.fee(), 21);
++test;
EXPECT_EQ(test.fee(), 22);
test--;
EXPECT_EQ(test.fee(), 21);
--test;
EXPECT_EQ(test.fee(), 20);
test *= 5;
EXPECT_EQ(test.fee(), 100);
test /= 2;
EXPECT_EQ(test.fee(), 50);
/* illegal with floating
test %= 13;
EXPECT_EQ(test.fee(), 11);
*/
// legal with signed
test = -test;
EXPECT_EQ(test.fee(), -50);
EXPECT_EQ(test.signum(), -1);
EXPECT_EQ(to_string(test), "-50.000000");
EXPECT_TRUE(test);
test = 0;
EXPECT_FALSE(test);
EXPECT_EQ(test.signum(), 0);
test = targetSame;
EXPECT_EQ(test.signum(), 1);
EXPECT_EQ(to_string(test), "200.000000");
}
}
TEST(UnitsTest, initial_xrp)
{
EXPECT_EQ(kInitialXrp.drops(), 100'000'000'000'000'000);
EXPECT_EQ(kInitialXrp, XRPAmount{100'000'000'000'000'000});
}
} // namespace xrpl::test

View File

@@ -0,0 +1,295 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/beast/utility/Zero.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
namespace xrpl {
TEST(XRPAmountTest, sig_num)
{
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
if (i < 0)
{
EXPECT_TRUE(x.signum() < 0);
}
else if (i > 0)
{
EXPECT_TRUE(x.signum() > 0);
}
else
{
EXPECT_EQ(x.signum(), 0);
}
}
}
TEST(XRPAmountTest, beast_zero)
{
using beast::kZero;
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
EXPECT_TRUE((i == 0) == (x == kZero));
EXPECT_TRUE((i != 0) == (x != kZero));
EXPECT_TRUE((i < 0) == (x < kZero));
EXPECT_TRUE((i > 0) == (x > kZero));
EXPECT_TRUE((i <= 0) == (x <= kZero));
EXPECT_TRUE((i >= 0) == (x >= kZero));
EXPECT_TRUE((0 == i) == (kZero == x));
EXPECT_TRUE((0 != i) == (kZero != x));
EXPECT_TRUE((0 < i) == (kZero < x));
EXPECT_TRUE((0 > i) == (kZero > x));
EXPECT_TRUE((0 <= i) == (kZero <= x));
EXPECT_TRUE((0 >= i) == (kZero >= x));
}
}
TEST(XRPAmountTest, comparisons)
{
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
for (auto j : {-1, 0, 1})
{
XRPAmount const y(j);
EXPECT_EQ((i == j), (x == y));
EXPECT_EQ((i != j), (x != y));
EXPECT_EQ((i < j), (x < y));
EXPECT_EQ((i > j), (x > y));
EXPECT_EQ((i <= j), (x <= y));
EXPECT_EQ((i >= j), (x >= y));
}
}
}
TEST(XRPAmountTest, add_sub)
{
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
for (auto j : {-1, 0, 1})
{
XRPAmount const y(j);
EXPECT_EQ(XRPAmount(i + j), (x + y));
EXPECT_EQ(XRPAmount(i - j), (x - y));
EXPECT_EQ((x + y), (y + x)); // addition is commutative
}
}
}
TEST(XRPAmountTest, decimal)
{
// Tautology
EXPECT_EQ(kDropsPerXrp.decimalXRP(), 1);
XRPAmount test{1};
EXPECT_EQ(test.decimalXRP(), 0.000001);
test = -test;
EXPECT_EQ(test.decimalXRP(), -0.000001);
test = 100'000'000;
EXPECT_EQ(test.decimalXRP(), 100);
test = -test;
EXPECT_EQ(test.decimalXRP(), -100);
}
TEST(XRPAmountTest, functions)
{
// Explicitly test every defined function for the XRPAmount class
// since some of them are templated, but not used anywhere else.
auto make = [&](auto x) -> XRPAmount { return XRPAmount{x}; };
XRPAmount const defaulted{};
(void)defaulted;
XRPAmount test{0};
EXPECT_EQ(test.drops(), 0);
test = make(beast::kZero);
EXPECT_EQ(test.drops(), 0);
test = beast::kZero;
EXPECT_EQ(test.drops(), 0);
test = make(100);
EXPECT_EQ(test.drops(), 100);
test = make(100u);
EXPECT_EQ(test.drops(), 100);
XRPAmount const targetSame{200u};
test = make(targetSame);
EXPECT_EQ(test.drops(), 200);
EXPECT_EQ(test, targetSame);
EXPECT_TRUE(test < XRPAmount{1000});
EXPECT_TRUE(test > XRPAmount{100});
test = std::int64_t(200);
EXPECT_EQ(test.drops(), 200);
test = std::uint32_t(300);
EXPECT_EQ(test.drops(), 300);
test = targetSame;
EXPECT_EQ(test.drops(), 200);
auto testOther = test.dropsAs<std::uint32_t>();
EXPECT_TRUE(testOther);
EXPECT_EQ(*testOther, 200); // NOLINT(bugprone-unchecked-optional-access)
test = std::numeric_limits<std::uint64_t>::max();
testOther = test.dropsAs<std::uint32_t>();
EXPECT_FALSE(testOther);
test = -1;
testOther = test.dropsAs<std::uint32_t>();
EXPECT_FALSE(testOther);
test = targetSame * 2;
EXPECT_EQ(test.drops(), 400);
test = 3 * targetSame;
EXPECT_EQ(test.drops(), 600);
test = 20;
EXPECT_EQ(test.drops(), 20);
test += targetSame;
EXPECT_EQ(test.drops(), 220);
test -= targetSame;
EXPECT_EQ(test.drops(), 20);
test *= 5;
EXPECT_EQ(test.drops(), 100);
test = 50;
EXPECT_EQ(test.drops(), 50);
test -= 39;
EXPECT_EQ(test.drops(), 11);
// legal with signed
test = -test;
EXPECT_EQ(test.drops(), -11);
EXPECT_EQ(test.signum(), -1);
EXPECT_EQ(to_string(test), "-11");
EXPECT_TRUE(test);
test = 0;
EXPECT_FALSE(test);
EXPECT_EQ(test.signum(), 0);
test = targetSame;
EXPECT_EQ(test.signum(), 1);
EXPECT_EQ(to_string(test), "200");
}
TEST(XRPAmountTest, mul_ratio)
{
constexpr auto kMaxUInt32 = std::numeric_limits<std::uint32_t>::max();
constexpr auto kMaxXrp = std::numeric_limits<XRPAmount::value_type>::max();
constexpr auto kMinXrp = std::numeric_limits<XRPAmount::value_type>::min();
{
// multiply by a number that would overflow then divide by the same
// number, and check we didn't lose any value
XRPAmount big(kMaxXrp);
EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, true));
// rounding mode shouldn't matter as the result is exact
EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, false));
// multiply and divide by values that would overflow if done
// naively, and check that it gives the correct answer
big -= 0xf; // Subtract a little so it's divisible by 4
EXPECT_EQ(mulRatio(big, 3, 4, false).value(), (big.value() / 4) * 3);
EXPECT_EQ(mulRatio(big, 3, 4, true).value(), (big.value() / 4) * 3);
EXPECT_EQ(big.value() % 4, 0);
EXPECT_GT(big.value(), kMaxXrp / 3);
EXPECT_LE(big.value() / 4, kMaxXrp / 3);
}
{
// Similar test as above, but for negative values
XRPAmount big(kMinXrp); // NOLINT TODO
EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, true));
// rounding mode shouldn't matter as the result is exact
EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, false));
// multiply and divide by values that would overflow if done
// naively, and check that it gives the correct answer
EXPECT_EQ(mulRatio(big, 3, 4, false).value(), (big.value() / 4) * 3);
EXPECT_EQ(mulRatio(big, 3, 4, true).value(), (big.value() / 4) * 3);
EXPECT_EQ(big.value() % 4, 0);
EXPECT_LT(big.value(), kMinXrp / 3);
EXPECT_GE(big.value() / 4, kMinXrp / 3);
}
{
// small amounts
XRPAmount const tiny(1);
// Round up should give the smallest allowable number
EXPECT_EQ(tiny, mulRatio(tiny, 1, kMaxUInt32, true));
// rounding down should be zero
EXPECT_EQ(beast::kZero, mulRatio(tiny, 1, kMaxUInt32, false));
EXPECT_EQ(beast::kZero, mulRatio(tiny, kMaxUInt32 - 1, kMaxUInt32, false));
// tiny negative numbers
XRPAmount const tinyNeg(-1);
// Round up should give zero
EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, 1, kMaxUInt32, true));
EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, true));
// rounding down should be tiny
EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, false));
}
{ // rounding
{
XRPAmount const one(1);
auto const rup = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, true);
auto const rdown = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, false);
EXPECT_EQ(rup.drops() - rdown.drops(), 1);
}
{
XRPAmount const big(kMaxXrp);
auto const rup = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, true);
auto const rdown = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, false);
EXPECT_EQ(rup.drops() - rdown.drops(), 1);
}
{
XRPAmount const negOne(-1);
auto const rup = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, true);
auto const rdown = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, false);
EXPECT_EQ(rup.drops() - rdown.drops(), 1);
}
}
{
// division by zero
XRPAmount const one(1);
EXPECT_ANY_THROW({ mulRatio(one, 1, 0, true); });
}
{
// overflow
XRPAmount const big(kMaxXrp);
EXPECT_ANY_THROW({ mulRatio(big, 2, 1, true); });
}
{
// underflow
XRPAmount const bigNegative(kMinXrp + 10);
EXPECT_EQ(mulRatio(bigNegative, 2, 1, true), kMinXrp);
}
}
} // namespace xrpl

View File

@@ -0,0 +1,438 @@
#include <xrpl/protocol/detail/token_errors.h>
#include <boost/multiprecision/cpp_int.hpp> // IWYU pragma: keep
#include <gtest/gtest.h>
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <ranges>
#include <stdexcept>
#include <string>
#include <tuple>
#include <vector>
#ifndef _MSC_VER
#include <xrpl/protocol/detail/b58_utils.h>
#include <xrpl/protocol/tokens.h>
#include <array>
#include <cstddef>
#include <random>
#include <span>
#include <sstream>
namespace xrpl::test {
namespace {
[[nodiscard]] inline auto
randEngine() -> std::mt19937&
{
static std::mt19937 kR = [] {
std::random_device rd;
return std::mt19937{rd()};
}();
return kR;
}
constexpr int kNumTokenTypeIndexes = 9;
[[nodiscard]] inline auto
tokenTypeAndSize(int i) -> std::tuple<xrpl::TokenType, std::size_t>
{
assert(i < kNumTokenTypeIndexes);
switch (i)
{
using enum xrpl::TokenType;
case 0:
return {None, 20};
case 1:
return {NodePublic, 32};
case 2:
return {NodePublic, 33};
case 3:
return {NodePrivate, 32};
case 4:
return {AccountID, 20};
case 5:
return {AccountPublic, 32};
case 6:
return {AccountPublic, 33};
case 7:
return {AccountSecret, 32};
case 8:
return {FamilySeed, 16};
default:
throw std::invalid_argument(
"Invalid token selection passed to tokenTypeAndSize() "
"in " __FILE__);
}
}
[[nodiscard]] inline auto
randomTokenTypeAndSize() -> std::tuple<xrpl::TokenType, std::size_t>
{
using namespace xrpl;
auto& rng = randEngine();
std::uniform_int_distribution<> d(0, 8);
return tokenTypeAndSize(d(rng));
}
// Return the token type and subspan of `d` to use as test data.
[[nodiscard]] inline auto
randomB256TestData(std::span<std::uint8_t> d)
-> std::tuple<xrpl::TokenType, std::span<std::uint8_t>>
{
auto& rng = randEngine();
std::uniform_int_distribution<std::uint8_t> dist(0, 255);
auto [tokType, tokSize] = randomTokenTypeAndSize();
std::generate(d.begin(), d.begin() + tokSize, [&] { return dist(rng); });
return {tokType, d.subspan(0, tokSize)};
}
inline void
printAsChar(std::span<std::uint8_t> a, std::span<std::uint8_t> b)
{
auto asString = [](std::span<std::uint8_t> s) {
std::string r;
r.resize(s.size());
std::ranges::copy(s, r.begin());
return r;
};
auto sa = asString(a);
auto sb = asString(b);
std::cerr << "\n\n" << sa << "\n" << sb << "\n";
}
inline void
printAsInt(std::span<std::uint8_t> a, std::span<std::uint8_t> b)
{
auto asString = [](std::span<std::uint8_t> s) -> std::string {
std::stringstream sstr;
for (auto i : s)
{
sstr << std::setw(3) << int(i) << ',';
}
return sstr.str();
};
auto sa = asString(a);
auto sb = asString(b);
std::cerr << "\n\n" << sa << "\n" << sb << "\n";
}
} // namespace
namespace multiprecision_utils {
boost::multiprecision::checked_uint512_t
toBoostMP(std::span<std::uint64_t> in)
{
boost::multiprecision::checked_uint512_t mbp = 0;
for (auto const& i : std::views::reverse(in))
{
mbp <<= 64;
mbp += i;
}
return mbp;
}
std::vector<std::uint64_t>
randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5)
{
auto eng = randEngine();
std::uniform_int_distribution<std::uint8_t> numCoeffDist(minSize, maxSize);
std::uniform_int_distribution<std::uint64_t> dist;
auto const numCoeff = numCoeffDist(eng);
std::vector<std::uint64_t> coeffs;
coeffs.reserve(numCoeff);
for (int i = 0; i < numCoeff; ++i)
{
coeffs.push_back(dist(eng));
}
return coeffs;
}
} // namespace multiprecision_utils
TEST(Base58Test, multiprecision)
{
using namespace boost::multiprecision;
constexpr std::size_t kIters = 100000;
auto eng = randEngine();
std::uniform_int_distribution<std::uint64_t> dist;
std::uniform_int_distribution<std::uint64_t> dist1(1);
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist(eng);
if (d == 0u)
continue;
auto bigInt = multiprecision_utils::randomBigInt();
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refDiv = boostBigInt / d;
auto const refMod = boostBigInt % d;
auto const mod = b58_fast::detail::inplaceBigintDivRem(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
auto const foundDiv = multiprecision_utils::toBoostMP(bigInt);
EXPECT_EQ(refMod.convert_to<std::uint64_t>(), mod);
EXPECT_EQ(foundDiv, refDiv);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist(eng);
auto bigInt = multiprecision_utils::randomBigInt(/*minSize*/ 2);
if (bigInt[bigInt.size() - 1] == std::numeric_limits<std::uint64_t>::max())
{
bigInt[bigInt.size() - 1] -= 1; // Prevent overflow
}
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refAdd = boostBigInt + d;
auto const result = b58_fast::detail::inplaceBigintAdd(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
EXPECT_EQ(result, TokenCodecErrc::Success);
auto const foundAdd = multiprecision_utils::toBoostMP(bigInt);
EXPECT_EQ(refAdd, foundAdd);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist1(eng);
// Force overflow
std::vector<std::uint64_t> bigInt(5, std::numeric_limits<std::uint64_t>::max());
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refAdd = boostBigInt + d;
auto const result = b58_fast::detail::inplaceBigintAdd(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
EXPECT_EQ(result, TokenCodecErrc::OverflowAdd);
auto const foundAdd = multiprecision_utils::toBoostMP(bigInt);
EXPECT_NE(refAdd, foundAdd);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist(eng);
auto bigInt = multiprecision_utils::randomBigInt(/* minSize */ 2);
// inplace mul requires the most significant coeff to be zero to
// hold the result.
bigInt[bigInt.size() - 1] = 0;
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refMul = boostBigInt * d;
auto const result = b58_fast::detail::inplaceBigintMul(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
EXPECT_EQ(result, TokenCodecErrc::Success);
auto const foundMul = multiprecision_utils::toBoostMP(bigInt);
EXPECT_EQ(refMul, foundMul);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist1(eng);
// Force overflow
std::vector<std::uint64_t> bigInt(5, std::numeric_limits<std::uint64_t>::max());
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refMul = boostBigInt * d;
auto const result = b58_fast::detail::inplaceBigintMul(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
EXPECT_EQ(result, TokenCodecErrc::InputTooLarge);
auto const foundMul = multiprecision_utils::toBoostMP(bigInt);
EXPECT_NE(refMul, foundMul);
}
}
TEST(Base58Test, fast_matches_ref)
{
auto testRawEncode = [&](std::span<std::uint8_t> const& b256Data) {
std::array<std::uint8_t, 64> b58ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b58Result;
std::array<std::uint8_t, 64> b256ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b256Result;
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b58ResultBuf[i]};
if (i == 0)
{
auto const r = xrpl::b58_fast::detail::b256ToB58Be(b256Data, outBuf);
EXPECT_TRUE(r);
b58Result[i] = r.value();
}
else
{
std::array<std::uint8_t, 128> tmpBuf{};
std::string const s = xrpl::b58_ref::detail::encodeBase58(
b256Data.data(), b256Data.size(), tmpBuf.data(), tmpBuf.size());
EXPECT_TRUE(s.size());
b58Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b58Result[i].begin());
}
}
auto const rawB58SameSize = b58Result[0].size() == b58Result[1].size();
EXPECT_TRUE(rawB58SameSize);
if (rawB58SameSize)
{
auto const rawB58SameData =
memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0;
EXPECT_TRUE(rawB58SameData);
if (!rawB58SameData)
{
printAsChar(b58Result[0], b58Result[1]);
}
}
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()};
if (i == 0)
{
std::string const in(
b58Result[i].data(), b58Result[i].data() + b58Result[i].size());
auto const r = xrpl::b58_fast::detail::b58ToB256Be(in, outBuf);
EXPECT_TRUE(r);
b256Result[i] = r.value();
}
else
{
std::string const st(b58Result[i].begin(), b58Result[i].end());
std::string const s = xrpl::b58_ref::detail::decodeBase58(st);
EXPECT_TRUE(s.size());
b256Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b256Result[i].begin());
}
}
auto const rawB256SameSize = b256Result[0].size() == b256Result[1].size();
EXPECT_TRUE(rawB256SameSize);
if (rawB256SameSize)
{
auto const rawB256SameData =
memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == 0;
EXPECT_TRUE(rawB256SameData);
if (!rawB256SameData)
{
printAsInt(b256Result[0], b256Result[1]);
}
}
};
auto testTokenEncode = [&](xrpl::TokenType const tokType,
std::span<std::uint8_t> const& b256Data) {
std::array<std::uint8_t, 64> b58ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b58Result;
std::array<std::uint8_t, 64> b256ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b256Result;
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b58ResultBuf[i].data(), b58ResultBuf[i].size()};
if (i == 0)
{
auto const r = xrpl::b58_fast::encodeBase58Token(tokType, b256Data, outBuf);
EXPECT_TRUE(r);
b58Result[i] = r.value();
}
else
{
std::string const s =
xrpl::b58_ref::encodeBase58Token(tokType, b256Data.data(), b256Data.size());
EXPECT_TRUE(s.size());
b58Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b58Result[i].begin());
}
}
auto const tokenB58SameSize = b58Result[0].size() == b58Result[1].size();
EXPECT_TRUE(tokenB58SameSize);
if (tokenB58SameSize)
{
auto const tokenB58SameData =
memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0;
EXPECT_TRUE(tokenB58SameData);
if (!tokenB58SameData)
{
printAsChar(b58Result[0], b58Result[1]);
}
}
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()};
if (i == 0)
{
std::string const in(
b58Result[i].data(), b58Result[i].data() + b58Result[i].size());
auto const r = xrpl::b58_fast::decodeBase58Token(tokType, in, outBuf);
EXPECT_TRUE(r);
b256Result[i] = r.value();
}
else
{
std::string const st(b58Result[i].begin(), b58Result[i].end());
std::string const s = xrpl::b58_ref::decodeBase58Token(st, tokType);
EXPECT_TRUE(s.size());
b256Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b256Result[i].begin());
}
}
auto const tokenB256SameSize = b256Result[0].size() == b256Result[1].size();
EXPECT_TRUE(tokenB256SameSize);
if (tokenB256SameSize)
{
auto const tokenB256SameData =
memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == 0;
EXPECT_TRUE(tokenB256SameData);
if (!tokenB256SameData)
{
printAsInt(b256Result[0], b256Result[1]);
}
}
};
auto testIt = [&](xrpl::TokenType const tokType, std::span<std::uint8_t> const& b256Data) {
testRawEncode(b256Data);
testTokenEncode(tokType, b256Data);
};
// test every token type with data where every byte is the same and the
// bytes range from 0-255
for (int i = 0; i < kNumTokenTypeIndexes; ++i)
{
std::array<std::uint8_t, 128> b256DataBuf{};
auto const [tokType, tokSize] = tokenTypeAndSize(i);
for (int d = 0; d <= 255; ++d)
{
memset(b256DataBuf.data(), d, tokSize);
testIt(tokType, std::span(b256DataBuf.data(), tokSize));
}
}
// test with random data
constexpr std::size_t kIters = 100000;
for (int i = 0; i < kIters; ++i)
{
std::array<std::uint8_t, 128> b256DataBuf{};
auto const [tokType, b256Data] = randomB256TestData(b256DataBuf);
testIt(tokType, b256Data);
}
}
} // namespace xrpl::test
#endif // _MSC_VER

View File

@@ -0,0 +1,360 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/utility/Zero.h>
#include <boost/endian/detail/order.hpp>
#include <gtest/gtest.h>
#include <array>
#include <cassert>
#include <complex>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <unordered_set>
#include <utility>
#include <vector>
namespace xrpl::test {
// a non-hashing Hasher that just copies the bytes.
// Used to test hash_append in base_uint
template <std::size_t Bits>
struct Nonhash
{
static constexpr auto const kEndian = boost::endian::order::big;
static constexpr std::size_t kWidth = Bits / 8;
std::array<std::uint8_t, kWidth> data;
Nonhash() = default;
void
operator()(void const* key, std::size_t len) noexcept
{
assert(len == kWidth);
memcpy(data.data(), key, len);
}
explicit
operator std::size_t() noexcept
{
return kWidth;
}
};
struct BaseUintTest : public ::testing::Test
{
using BaseUInt96 = BaseUInt<96>;
static_assert(std::is_copy_constructible_v<BaseUInt96>);
static_assert(std::is_copy_assignable_v<BaseUInt96>);
static void
testComparisons()
{
{
static constexpr std::array<std::pair<std::string_view, std::string_view>, 6> kTestArgs{
{{"0000000000000000", "0000000000000001"},
{"0000000000000000", "ffffffffffffffff"},
{"1234567812345678", "2345678923456789"},
{"8000000000000000", "8000000000000001"},
{"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"},
{"fffffffffffffffe", "ffffffffffffffff"}}};
for (auto const& arg : kTestArgs)
{
xrpl::BaseUInt<64> const u{arg.first}, v{arg.second};
// For code readability, we want to use general boolean
// expectations instead of specific EXPECT_LT etc.
EXPECT_TRUE(u < v);
EXPECT_TRUE(u <= v);
EXPECT_TRUE(u != v);
EXPECT_FALSE(u == v);
EXPECT_FALSE(u > v);
EXPECT_FALSE(u >= v);
EXPECT_FALSE(v < u);
EXPECT_FALSE(v <= u);
EXPECT_TRUE(v != u);
EXPECT_FALSE(v == u);
EXPECT_TRUE(v > u);
EXPECT_TRUE(v >= u);
EXPECT_TRUE(u == u);
EXPECT_TRUE(v == v);
}
}
{
static constexpr std::array<std::pair<std::string_view, std::string_view>, 6> kTestArgs{
{
{"000000000000000000000000", "000000000000000000000001"},
{"000000000000000000000000", "ffffffffffffffffffffffff"},
{"0123456789ab0123456789ab", "123456789abc123456789abc"},
{"555555555555555555555555", "55555555555a555555555555"},
{"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"},
{"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"},
}};
for (auto const& arg : kTestArgs)
{
xrpl::BaseUInt<96> const u{arg.first}, v{arg.second};
EXPECT_TRUE(u < v);
EXPECT_TRUE(u <= v);
EXPECT_TRUE(u != v);
EXPECT_FALSE(u == v);
EXPECT_FALSE(u > v);
EXPECT_FALSE(u >= v);
EXPECT_FALSE(v < u);
EXPECT_FALSE(v <= u);
EXPECT_TRUE(v != u);
EXPECT_FALSE(v == u);
EXPECT_TRUE(v > u);
EXPECT_TRUE(v >= u);
EXPECT_TRUE(u == u);
EXPECT_TRUE(v == v);
}
}
}
};
TEST_F(BaseUintTest, base_uint)
{
static_assert(!std::is_constructible_v<BaseUInt96, std::complex<double>>);
static_assert(!std::is_assignable_v<BaseUInt96&, std::complex<double>>);
testComparisons();
// used to verify set insertion (hashing required)
std::unordered_set<BaseUInt96, HardenedHash<>> uset;
Blob const raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
EXPECT_EQ(BaseUInt96::kBytes, raw.size());
BaseUInt96 u = BaseUInt96::fromRaw(raw);
uset.insert(u);
EXPECT_EQ(raw.size(), u.size());
EXPECT_EQ(to_string(u), "0102030405060708090A0B0C");
EXPECT_EQ(toShortString(u), "01020304...");
EXPECT_EQ(*u.data(), 1);
EXPECT_EQ(u.signum(), 1);
EXPECT_FALSE(!u);
EXPECT_FALSE(u.isZero());
EXPECT_TRUE(u.isNonZero());
unsigned char t = 0;
for (auto& d : u)
{
EXPECT_EQ(d, ++t);
}
// Test hash_append by "hashing" with a no-op hasher (h)
// and then extracting the bytes that were written during hashing
// back into another base_uint (w) for comparison with the original
Nonhash<96> h{};
hash_append(h, u);
BaseUInt96 const w =
BaseUInt96::fromRaw(std::vector<std::uint8_t>(h.data.begin(), h.data.end()));
EXPECT_EQ(w, u);
BaseUInt96 v{~u};
uset.insert(v);
EXPECT_EQ(to_string(v), "FEFDFCFBFAF9F8F7F6F5F4F3");
EXPECT_EQ(toShortString(v), "FEFDFCFB...");
EXPECT_EQ(*v.data(), 0xfe);
EXPECT_EQ(v.signum(), 1);
EXPECT_FALSE(!v);
EXPECT_FALSE(v.isZero());
EXPECT_TRUE(v.isNonZero());
t = 0xff;
for (auto& d : v)
{
EXPECT_EQ(d, --t);
}
EXPECT_LT(u, v);
EXPECT_GT(v, u);
v = u;
EXPECT_EQ(v, u);
BaseUInt96 z{beast::kZero};
uset.insert(z);
EXPECT_EQ(to_string(z), "000000000000000000000000");
EXPECT_EQ(toShortString(z), "00000000...");
EXPECT_EQ(*z.data(), 0);
EXPECT_EQ(*z.begin(), 0);
EXPECT_EQ(*std::prev(z.end(), 1), 0);
EXPECT_EQ(z.signum(), 0);
EXPECT_TRUE(!z);
EXPECT_TRUE(z.isZero());
EXPECT_FALSE(z.isNonZero());
for (auto& d : z)
{
EXPECT_EQ(d, 0);
}
BaseUInt96 n{z};
n++;
EXPECT_EQ(n, BaseUInt96(1));
n--;
EXPECT_EQ(n, beast::kZero);
EXPECT_EQ(n, z);
n--;
EXPECT_EQ(to_string(n), "FFFFFFFFFFFFFFFFFFFFFFFF");
EXPECT_EQ(toShortString(n), "FFFFFFFF...");
n = beast::kZero;
EXPECT_EQ(n, z);
BaseUInt96 zp1{z};
zp1++;
BaseUInt96 zm1{z};
zm1--;
BaseUInt96 const x{zm1 ^ zp1};
uset.insert(x);
EXPECT_EQ(to_string(x), "FFFFFFFFFFFFFFFFFFFFFFFE") << to_string(x);
EXPECT_EQ(toShortString(x), "FFFFFFFF...") << toShortString(x);
EXPECT_EQ(uset.size(), 4);
BaseUInt96 tmp;
EXPECT_TRUE(tmp.parseHex(to_string(u)));
EXPECT_EQ(tmp, u);
tmp = z;
// fails with extra char
EXPECT_FALSE(tmp.parseHex("A" + to_string(u)));
tmp = z;
// fails with extra char at end
EXPECT_FALSE(tmp.parseHex(to_string(u) + "A"));
// fails with a non-hex character at some point in the string:
tmp = z;
for (std::size_t i = 0; i != 24; ++i)
{
std::string x = to_string(z);
x[i] = ('G' + (i % 10));
EXPECT_FALSE(tmp.parseHex(x));
}
// Walking 1s:
for (std::size_t i = 0; i != 24; ++i)
{
std::string s1 = "000000000000000000000000";
s1[i] = '1';
EXPECT_TRUE(tmp.parseHex(s1));
EXPECT_EQ(to_string(tmp), s1);
}
// Walking 0s:
for (std::size_t i = 0; i != 24; ++i)
{
std::string s1 = "111111111111111111111111";
s1[i] = '0';
EXPECT_TRUE(tmp.parseHex(s1));
EXPECT_EQ(to_string(tmp), s1);
}
// Constexpr constructors
{
static_assert(BaseUInt96{}.signum() == 0);
static_assert(BaseUInt96("0").signum() == 0);
static_assert(BaseUInt96("000000000000000000000000").signum() == 0);
static_assert(BaseUInt96("000000000000000000000001").signum() == 1);
static_assert(BaseUInt96("800000000000000000000000").signum() == 1);
// Everything within the #if should fail during compilation.
#if 0
// Too few characters
static_assert(BaseUInt96("00000000000000000000000").signum() == 0);
// Too many characters
static_assert(BaseUInt96("0000000000000000000000000").signum() == 0);
// Non-hex characters
static_assert(BaseUInt96("00000000000000000000000 ").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000/").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000:").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000@").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000G").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000`").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000g").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000~").signum() == 1);
#endif // 0
// Using the constexpr constructor in a non-constexpr context
// with an error in the parsing throws an exception.
{
// Invalid length for string.
bool caught = false;
try
{
// Try to prevent constant evaluation.
std::vector<char> str(23, '7');
std::string_view const sView(str.data(), str.size());
[[maybe_unused]] BaseUInt96 const t96(sView);
}
catch (std::invalid_argument const& e)
{
EXPECT_EQ(e.what(), std::string("invalid length for hex string"));
caught = true;
}
EXPECT_TRUE(caught);
}
{
// Invalid character in string.
bool caught = false;
try
{
// Try to prevent constant evaluation.
std::vector<char> str(23, '7');
str.push_back('G');
std::string_view const sView(str.data(), str.size());
[[maybe_unused]] BaseUInt96 const t96(sView);
}
catch (std::range_error const& e)
{
EXPECT_EQ(e.what(), std::string("invalid hex character"));
caught = true;
}
EXPECT_TRUE(caught);
}
// Verify that constexpr base_uints interpret a string the same
// way parseHex() does.
struct StrBaseUInt
{
char const* const str;
BaseUInt96 tst;
constexpr StrBaseUInt(char const* s) : str(s), tst(s)
{
}
};
constexpr StrBaseUInt kTestCases[] = {
"000000000000000000000000",
"000000000000000000000001",
"fedcba9876543210ABCDEF91",
"19FEDCBA0123456789abcdef",
"800000000000000000000000",
"fFfFfFfFfFfFfFfFfFfFfFfF",
};
for (StrBaseUInt const& t : kTestCases)
{
BaseUInt96 t96;
EXPECT_TRUE(t96.parseHex(t.str));
EXPECT_EQ(t96, t.tst);
}
}
}
} // namespace xrpl::test

View File

@@ -0,0 +1,227 @@
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/hash/hash_append.h>
#include <gtest/gtest.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <ios>
#include <ostream>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
namespace xrpl::detail {
template <class T>
class TestUserTypeMember
{
private:
T t_;
public:
explicit TestUserTypeMember(T const& t = T()) : t_(t)
{
}
template <class Hasher>
friend void
// NOLINTNEXTLINE(readability-identifier-naming)
hash_append(Hasher& h, TestUserTypeMember const& a) noexcept
{
using beast::hash_append;
hash_append(h, a.t_);
}
};
template <class T>
class TestUserTypeFree
{
private:
T t_;
public:
explicit TestUserTypeFree(T const& t = T()) : t_(t)
{
}
template <class Hasher>
friend void
// NOLINTNEXTLINE(readability-identifier-naming)
hash_append(Hasher& h, TestUserTypeFree const& a) noexcept
{
using beast::hash_append;
hash_append(h, a.t_);
}
};
} // namespace xrpl::detail
//------------------------------------------------------------------------------
namespace xrpl {
namespace detail {
template <class T>
using test_hardened_unordered_set = std::unordered_set<T, HardenedHash<>>;
template <class T>
using test_hardened_unordered_map = std::unordered_map<T, int, HardenedHash<>>;
template <class T>
using test_hardened_unordered_multiset = std::unordered_multiset<T, HardenedHash<>>;
template <class T>
using test_hardened_unordered_multimap = std::unordered_multimap<T, int, HardenedHash<>>;
} // namespace detail
template <std::size_t Bits, class UInt = std::uint64_t>
class UnsignedInteger
{
private:
static_assert(
std::is_integral_v<UInt> && std::is_unsigned_v<UInt>,
"UInt must be an unsigned integral type");
static_assert(Bits % (8 * sizeof(UInt)) == 0, "Bits must be a multiple of 8*sizeof(UInt)");
static_assert(Bits >= (8 * sizeof(UInt)), "Bits must be at least 8*sizeof(UInt)");
static std::size_t const kSize = Bits / (8 * sizeof(UInt));
std::array<UInt, kSize> vec_;
public:
using value_type = UInt;
static std::size_t const kBits = Bits;
static std::size_t const kBytes = kBits / 8;
template <class Int>
static UnsignedInteger
fronumber(Int v)
{
UnsignedInteger result;
for (std::size_t i(1); i < kSize; ++i)
result.vec_[i] = 0;
result.vec_[0] = v;
return result;
}
void*
data() noexcept
{
return &vec_[0];
}
[[nodiscard]] void const*
data() const noexcept
{
return &vec_[0];
}
template <class Hasher>
friend void
hash_append(Hasher& h, UnsignedInteger const& a) noexcept
{
using beast::hash_append;
hash_append(h, a.vec_);
}
friend std::ostream&
operator<<(std::ostream& s, UnsignedInteger const& v)
{
for (std::size_t i(0); i < kSize; ++i)
s << std::hex << std::setfill('0') << std::setw(2 * sizeof(UInt)) << v.vec_[i];
return s;
}
};
using sha256_t = UnsignedInteger<256, std::size_t>;
#ifndef __INTELLISENSE__
static_assert(sha256_t::kBits == 256, "sha256_t must have 256 bits");
#endif
} // namespace xrpl
//------------------------------------------------------------------------------
namespace xrpl {
class HardenedHashTest : public ::testing::Test
{
public:
template <class T>
static void
check()
{
T t{};
HardenedHash<>()(t);
SUCCEED();
}
template <template <class T> class U>
static void
checkUserType()
{
check<U<bool>>();
check<U<char>>();
check<U<signed char>>();
check<U<unsigned char>>();
// These cause trouble for boost
// check <U <char16_t>> ();
// check <U <char32_t>> ();
check<U<wchar_t>>();
check<U<short>>();
check<U<unsigned short>>();
check<U<int>>();
check<U<unsigned int>>();
check<U<long>>();
check<U<long long>>();
check<U<unsigned long>>();
check<U<unsigned long long>>();
check<U<float>>();
check<U<double>>();
check<U<long double>>();
}
template <template <class T> class C>
static void
checkContainer()
{
{
C<detail::TestUserTypeMember<std::string>> const c;
}
SUCCEED();
{
C<detail::TestUserTypeFree<std::string>> const c;
}
SUCCEED();
}
};
TEST_F(HardenedHashTest, user_types)
{
checkUserType<detail::TestUserTypeMember>();
checkUserType<detail::TestUserTypeFree>();
}
TEST_F(HardenedHashTest, containers)
{
checkContainer<detail::test_hardened_unordered_set>();
checkContainer<detail::test_hardened_unordered_map>();
checkContainer<detail::test_hardened_unordered_multiset>();
checkContainer<detail::test_hardened_unordered_multimap>();
}
} // namespace xrpl

View File

@@ -0,0 +1,80 @@
#include <xrpl/basics/join.h>
#include <xrpl/basics/base_uint.h>
#include <gtest/gtest.h>
#include <array>
#include <cstddef>
#include <initializer_list>
#include <sstream>
#include <string>
#include <vector>
namespace xrpl::test {
struct JoinTest : public ::testing::Test
{
};
TEST_F(JoinTest, join)
{
auto test = [](auto collectionanddelimiter, std::string expected) {
std::stringstream ss;
// Put something else in the buffer before and after to ensure that
// the << operator returns the stream correctly.
ss << "(" << collectionanddelimiter << ")";
auto const str = ss.str();
EXPECT_EQ(str.substr(1, str.length() - 2), expected);
EXPECT_EQ(str.front(), '(');
EXPECT_EQ(str.back(), ')');
};
// C++ array
test(CollectionAndDelimiter(std::array<int, 4>{2, -1, 5, 10}, "/"), "2/-1/5/10");
// One item C++ array edge case
test(CollectionAndDelimiter(std::array<std::string, 1>{"test"}, " & "), "test");
// Empty C++ array edge case
test(CollectionAndDelimiter(std::array<int, 0>{}, ","), "");
{
// C-style array
char letters[4]{'w', 'a', 's', 'd'};
test(CollectionAndDelimiter(letters, std::to_string(0)), "w0a0s0d");
}
{
// Auto sized C-style array
std::string words[]{"one", "two", "three", "four"};
test(CollectionAndDelimiter(words, "\n"), "one\ntwo\nthree\nfour");
}
{
// One item C-style array edge case
std::string words[]{"thing"};
test(CollectionAndDelimiter(words, "\n"), "thing");
}
// Initializer list
test(CollectionAndDelimiter(std::initializer_list<size_t>{19, 25}, "+"), "19+25");
// vector
test(CollectionAndDelimiter(std::vector<int>{0, 42}, std::to_string(99)), "09942");
// vector with one item edge case
test(CollectionAndDelimiter(std::vector<std::string>{"master"}, "xxx"), "master");
// vector with one non-trivial streamable item edge case
test(
CollectionAndDelimiter(std::vector<uint256>{uint256{1}}, "xxx"),
"0000000000000000000000000000000000000000000000000000000000000001");
// empty vector edge case
test(CollectionAndDelimiter(std::vector<uint256>{}, ","), "");
// C-style string
test(CollectionAndDelimiter("string", " "), "s t r i n g");
// Empty C-style string edge case
test(CollectionAndDelimiter("", "*"), "");
// Single char C-style string edge case
test(CollectionAndDelimiter("x", "*"), "x");
// std::string
test(CollectionAndDelimiter(std::string{"string"}, "-"), "s-t-r-i-n-g");
// Empty std::string edge case
test(CollectionAndDelimiter(std::string{""}, "*"), "");
// Single char std::string edge case
test(CollectionAndDelimiter(std::string{"y"}, "*"), "y");
}
} // namespace xrpl::test

View File

@@ -80,7 +80,9 @@ TxTest::TxTest(std::optional<FeatureBitset> features)
std::vector<uint256>{featureSet_.begin(), featureSet_.end()},
registry_.getNodeFamily());
// Initialize time from the genesis ledger
// Initialize time from the genesis ledger. closedLedger_ is created above
// in the body, so this cannot be a member initializer.
// NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
now_ = closedLedger_->header().closeTime;
// Create an open view on top of the genesis ledger

View File

@@ -13,6 +13,7 @@
#include <exception>
#include <limits>
#include <numbers>
#include <optional>
#include <regex>
#include <sstream>
#include <string>
@@ -592,6 +593,57 @@ TEST(json_value, bad_json)
EXPECT_TRUE(r.parse(s, j));
}
namespace {
std::optional<json::Value>
parseValue(std::string const& doc)
{
json::Value j;
json::Reader r;
if (!r.parse("{\"v\":" + doc + "}", j))
return std::nullopt;
return j["v"];
}
} // namespace
TEST(json_value, parse_double_valid)
{
// 1e300 is large but still representable, so it parses (unlike the out-of-range cases below).
for (auto const& [text, expected] :
{std::pair{"2.5", 2.5},
std::pair{"-3.25e2", -325.0},
std::pair{"0.0", 0.0},
std::pair{"1E3", 1000.0},
std::pair{"-0.5e-1", -0.05},
std::pair{"1e300", 1e300}})
{
auto const v = parseValue(text);
ASSERT_TRUE(v.has_value()) << text;
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(v->isDouble()) << text;
EXPECT_EQ(v->asDouble(), expected) << text;
// NOLINTEND(bugprone-unchecked-optional-access)
}
}
TEST(json_value, parse_double_out_of_range)
{
// Magnitudes with no finite double representation are rejected.
for (char const* oor : {"1e400", "-1e400", "0.001e500", "1e-400", "-1e-400", "123e-500"})
EXPECT_FALSE(parseValue(oor).has_value()) << oor;
}
TEST(json_value, parse_double_malformed)
{
// readNumber() collects any run of digits and '.eE+-' into a single Double
// token, so these malformed tokens reach decodeDouble. Each has a valid
// leading prefix that from_chars would accept on its own; requiring the
// entire token be consumed rejects them instead of silently truncating.
for (char const* bad : {"1+2", "1-2", "1.2.3", "1e5e6", "1..2", "++5", "1e", "1e+", ".", "-"})
EXPECT_FALSE(parseValue(bad).has_value()) << bad;
}
TEST(json_value, edge_cases)
{
std::uint32_t const maxUInt = std::numeric_limits<std::uint32_t>::max();

View File

@@ -0,0 +1,241 @@
#include <xrpl/resource/detail/Logic.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/insight/NullCollector.h>
#include <xrpl/beast/net/IPAddressV4.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/resource/Charge.h>
#include <xrpl/resource/Consumer.h>
#include <xrpl/resource/Disposition.h>
#include <xrpl/resource/Gossip.h>
#include <xrpl/resource/detail/Tuning.h>
#include <boost/utility/base_from_member.hpp>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <chrono>
#include <cstdint>
#include <string>
namespace xrpl::Resource {
class ResourceManagerTest : public ::testing::Test
{
protected:
beast::Journal const j_{TestSink::instance()};
class TestLogic : private boost::base_from_member<TestStopwatch>, public Logic
{
private:
using clock_type = boost::base_from_member<TestStopwatch>;
public:
explicit TestLogic(beast::Journal journal)
: Logic(beast::insight::NullCollector::make(), member, journal)
{
}
void
advance()
{
++member;
}
TestStopwatch&
clock()
{
return member;
}
};
//--------------------------------------------------------------------------
static void
populateGossip(Gossip& gossip)
{
std::uint8_t const v(10 + randInt(9));
std::uint8_t const n(10 + randInt(9));
gossip.items.reserve(n);
for (std::uint8_t i = 0; i < n; ++i)
{
Gossip::Item item;
item.balance = 100 + randInt(499);
beast::IP::AddressV4::bytes_type const d = {{
192,
0,
2,
static_cast<std::uint8_t>(v + i),
}};
item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}};
gossip.items.push_back(item);
}
}
};
TEST_F(ResourceManagerTest, limited_warn_drop)
{
TestLogic logic{j_};
Charge const fee{kDropThreshold + 1};
beast::IP::Endpoint const addr{beast::IP::Endpoint::fromString("192.0.2.2")};
{
Consumer c{logic.newInboundEndpoint(addr)};
// Create load until we get a warning
int n = 10000;
bool warned = false;
while (--n >= 0)
{
if (c.charge(fee) == Disposition::Warn)
{
warned = true;
break;
}
++logic.clock();
}
ASSERT_TRUE(warned) << "Loop count exceeded without warning";
// Create load until we get dropped
bool dropped = false;
while (--n >= 0)
{
if (c.charge(fee) == Disposition::Drop)
{
dropped = true;
// Disconnect abusive Consumer
EXPECT_TRUE(c.disconnect(j_));
break;
}
++logic.clock();
}
ASSERT_TRUE(dropped) << "Loop count exceeded without dropping";
}
// Make sure the consumer is on the blacklist for a while.
{
Consumer const c{logic.newInboundEndpoint(addr)};
logic.periodicActivity();
EXPECT_EQ(c.disposition(), Disposition::Drop) << "Dropped consumer not put on blacklist";
}
// Makes sure the Consumer is eventually removed from blacklist
bool readmitted = false;
{
using namespace std::chrono_literals;
// Give Consumer time to become readmitted. Should never
// exceed expiration time.
auto n = kSecondsUntilExpiration + 1s;
while (--n > 0s)
{
++logic.clock();
logic.periodicActivity();
Consumer const c{logic.newInboundEndpoint(addr)};
if (c.disposition() != Disposition::Drop)
{
readmitted = true;
break;
}
}
}
EXPECT_TRUE(readmitted) << "Dropped Consumer left on blacklist too long";
}
TEST_F(ResourceManagerTest, unlimited_warn_drop)
{
TestLogic logic{j_};
Charge const fee{kDropThreshold + 1};
beast::IP::Endpoint const addr{beast::IP::Endpoint::fromString("192.0.2.2")};
Consumer c{logic.newUnlimitedEndpoint(addr)};
// Create load until we get a warning
int n = 10000;
bool warned = false;
while (--n >= 0)
{
if (c.charge(fee) == Disposition::Warn)
{
warned = true;
break;
}
++logic.clock();
}
EXPECT_FALSE(warned) << "Should loop forever with no warning";
}
TEST_F(ResourceManagerTest, charges)
{
TestLogic logic{j_};
{
beast::IP::Endpoint const address{beast::IP::Endpoint::fromString("192.0.2.1")};
Consumer c{logic.newInboundEndpoint(address)};
Charge const fee{1000};
JLOG(j_.info()) << "Charging " << c.toString() << " " << fee << " per second";
c.charge(fee);
for (int i = 0; i < 128; ++i)
{
JLOG(j_.info()) << "Time= " << logic.clock().now().time_since_epoch().count()
<< ", Balance = " << c.balance();
logic.advance();
}
}
{
beast::IP::Endpoint const address{beast::IP::Endpoint::fromString("192.0.2.2")};
Consumer c{logic.newInboundEndpoint(address)};
Charge const fee{1000};
JLOG(j_.info()) << "Charging " << c.toString() << " " << fee << " per second";
for (int i = 0; i < 128; ++i)
{
c.charge(fee);
JLOG(j_.info()) << "Time= " << logic.clock().now().time_since_epoch().count()
<< ", Balance = " << c.balance();
logic.advance();
}
}
}
TEST_F(ResourceManagerTest, imports)
{
TestLogic logic{j_};
Gossip g[5];
for (auto& i : g)
populateGossip(i);
for (int i = 0; i < 5; ++i)
logic.importConsumers(std::to_string(i), g[i]);
}
TEST_F(ResourceManagerTest, import)
{
TestLogic logic{j_};
Gossip g;
Gossip::Item item;
item.balance = 100;
beast::IP::AddressV4::bytes_type const d = {{
192,
0,
2,
1,
}};
item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}};
g.items.push_back(item);
logic.importConsumers("g", g);
}
} // namespace xrpl::Resource

View File

@@ -0,0 +1,22 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <shamap/common.h>
#include <memory>
namespace xrpl::tests {
TEST(FetchPackTest, construct_table)
{
beast::Journal const j{TestSink::instance()};
TestNodeFamily f{j};
std::shared_ptr<SHAMap> const t1{std::make_shared<SHAMap>(SHAMapType::FREE, f)};
EXPECT_NE(t1, nullptr);
}
} // namespace xrpl::tests

View File

@@ -0,0 +1,349 @@
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/SHAMapHash.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/shamap/SHAMapInnerNode.h>
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapLeafNode.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <shamap/common.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl::tests {
#ifndef __INTELLISENSE__
static_assert(std::is_nothrow_destructible<SHAMap>{});
static_assert(!std::is_default_constructible<SHAMap>{});
static_assert(!std::is_copy_constructible<SHAMap>{});
static_assert(!std::is_copy_assignable<SHAMap>{});
static_assert(!std::is_move_constructible<SHAMap>{});
static_assert(!std::is_move_assignable<SHAMap>{});
static_assert(std::is_nothrow_destructible<SHAMap::ConstIterator>{});
static_assert(std::is_copy_constructible<SHAMap::ConstIterator>{});
static_assert(std::is_copy_assignable<SHAMap::ConstIterator>{});
static_assert(std::is_move_constructible<SHAMap::ConstIterator>{});
static_assert(std::is_move_assignable<SHAMap::ConstIterator>{});
static_assert(std::is_nothrow_destructible<SHAMapItem>{});
static_assert(!std::is_default_constructible<SHAMapItem>{});
static_assert(!std::is_copy_constructible<SHAMapItem>{});
static_assert(std::is_nothrow_destructible<SHAMapNodeID>{});
static_assert(std::is_default_constructible<SHAMapNodeID>{});
static_assert(std::is_copy_constructible<SHAMapNodeID>{});
static_assert(std::is_copy_assignable<SHAMapNodeID>{});
static_assert(std::is_move_constructible<SHAMapNodeID>{});
static_assert(std::is_move_assignable<SHAMapNodeID>{});
static_assert(std::is_nothrow_destructible<SHAMapHash>{});
static_assert(std::is_default_constructible<SHAMapHash>{});
static_assert(std::is_copy_constructible<SHAMapHash>{});
static_assert(std::is_copy_assignable<SHAMapHash>{});
static_assert(std::is_move_constructible<SHAMapHash>{});
static_assert(std::is_move_assignable<SHAMapHash>{});
static_assert(std::is_nothrow_destructible<SHAMapTreeNode>{});
static_assert(!std::is_default_constructible<SHAMapTreeNode>{});
static_assert(!std::is_copy_constructible<SHAMapTreeNode>{});
static_assert(!std::is_copy_assignable<SHAMapTreeNode>{});
static_assert(!std::is_move_constructible<SHAMapTreeNode>{});
static_assert(!std::is_move_assignable<SHAMapTreeNode>{});
static_assert(std::is_nothrow_destructible<SHAMapInnerNode>{});
static_assert(!std::is_default_constructible<SHAMapInnerNode>{});
static_assert(!std::is_copy_constructible<SHAMapInnerNode>{});
static_assert(!std::is_copy_assignable<SHAMapInnerNode>{});
static_assert(!std::is_move_constructible<SHAMapInnerNode>{});
static_assert(!std::is_move_assignable<SHAMapInnerNode>{});
static_assert(std::is_nothrow_destructible<SHAMapLeafNode>{});
static_assert(!std::is_default_constructible<SHAMapLeafNode>{});
static_assert(!std::is_copy_constructible<SHAMapLeafNode>{});
static_assert(!std::is_copy_assignable<SHAMapLeafNode>{});
static_assert(!std::is_move_constructible<SHAMapLeafNode>{});
static_assert(!std::is_move_assignable<SHAMapLeafNode>{});
#endif
inline bool
operator!=(SHAMapItem const& a, SHAMapItem const& b)
{
return a.key() != b.key();
}
struct SHAMapBackingMode
{
bool backed;
std::string_view testName;
};
constexpr SHAMapBackingMode kBackedMode{.backed = true, .testName = "backed"};
constexpr SHAMapBackingMode kUnbackedMode{.backed = false, .testName = "unbacked"};
std::string
shamapBackingModeName(::testing::TestParamInfo<SHAMapBackingMode> const& info)
{
return std::string{info.param.testName};
}
class SHAMapTest : public ::testing::TestWithParam<SHAMapBackingMode>
{
protected:
beast::Journal const j_{TestSink::instance()};
static Buffer
intToVuc(std::uint8_t v)
{
Buffer vuc{32};
std::fill_n(vuc.data(), vuc.size(), v);
return vuc;
}
};
TEST_P(SHAMapTest, add_traverse_snapshot_build_tear_and_iterate)
{
auto const testMode = GetParam();
tests::TestNodeFamily f{j_};
// kH3 and kH4 differ only in the leaf, same terminal node (level 19)
constexpr uint256 kH1("092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 kH2("436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
constexpr uint256 kH3("b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
constexpr uint256 kH4("b92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
SHAMap sMap{SHAMapType::FREE, f};
sMap.invariants();
if (!testMode.backed)
sMap.setUnbacked();
auto i1 = makeShamapitem(kH1, intToVuc(1));
auto i2 = makeShamapitem(kH2, intToVuc(2));
auto i3 = makeShamapitem(kH3, intToVuc(3));
auto i4 = makeShamapitem(kH4, intToVuc(4));
EXPECT_TRUE(sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i2))) << "no add";
sMap.invariants();
EXPECT_TRUE(sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i1))) << "no add";
sMap.invariants();
auto i = sMap.begin();
auto e = sMap.end();
EXPECT_FALSE(i == e || (*i != *i1)) << "bad traverse";
++i;
EXPECT_FALSE(i == e || (*i != *i2)) << "bad traverse";
++i;
EXPECT_EQ(i, e) << "bad traverse";
sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i4));
sMap.invariants();
sMap.delItem(i2->key());
sMap.invariants();
sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i3));
sMap.invariants();
i = sMap.begin();
e = sMap.end();
EXPECT_FALSE(i == e || (*i != *i1)) << "bad traverse";
++i;
EXPECT_FALSE(i == e || (*i != *i3)) << "bad traverse";
++i;
EXPECT_FALSE(i == e || (*i != *i4)) << "bad traverse";
++i;
EXPECT_EQ(i, e) << "bad traverse";
SHAMapHash const mapHash = sMap.getHash();
std::shared_ptr<SHAMap> const map2 = sMap.snapShot(false);
map2->invariants();
EXPECT_EQ(sMap.getHash(), mapHash) << "bad snapshot";
EXPECT_EQ(map2->getHash(), mapHash) << "bad snapshot";
SHAMap::Delta delta;
ASSERT_TRUE(sMap.compare(*map2, delta, 100));
EXPECT_TRUE(delta.empty());
EXPECT_TRUE(sMap.delItem(sMap.begin()->key())) << "bad mod";
sMap.invariants();
EXPECT_NE(sMap.getHash(), mapHash) << "bad snapshot";
EXPECT_EQ(map2->getHash(), mapHash) << "bad snapshot";
ASSERT_TRUE(sMap.compare(*map2, delta, 100));
ASSERT_EQ(delta.size(), 1);
EXPECT_EQ(delta.begin()->first, kH1);
EXPECT_EQ(delta.begin()->second.first, nullptr);
ASSERT_NE(delta.begin()->second.second, nullptr);
EXPECT_EQ(delta.begin()->second.second->key(), kH1);
sMap.dump();
{
constexpr std::array kKeys{
uint256{"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92881fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92691fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92791fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b91891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b99891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"f22891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"292891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
};
constexpr std::array kHashes{
uint256{"B7387CFEA0465759ADC718E8C42B52D2309D179B326E239EB5075C64B6281F7F"},
uint256{"FBC195A9592A54AB44010274163CB6BA95F497EC5BA0A8831845467FB2ECE266"},
uint256{"4E7D2684B65DFD48937FFB775E20175C43AF0C94066F7D5679F51AE756795B75"},
uint256{"7A2F312EB203695FFD164E038E281839EEF06A1B99BFC263F3CECC6C74F93E07"},
uint256{"395A6691A372387A703FB0F2C6D2C405DAF307D0817F8F0E207596462B0E3A3E"},
uint256{"D044C0A696DE3169CC70AE216A1564D69DE96582865796142CE7D98A84D9DDE4"},
uint256{"76DCC77C4027309B5A91AD164083264D70B77B5E43E08AEDA5EBF94361143615"},
uint256{"DF4220E93ADC6F5569063A01B4DC79F8DB9553B6A3222ADE23DEA02BBE7230E5"},
};
SHAMap map{SHAMapType::FREE, f};
if (!testMode.backed)
map.setUnbacked();
EXPECT_EQ(map.getHash(), beast::kZero);
for (std::size_t k = 0; k < kKeys.size(); ++k)
{
EXPECT_TRUE(map.addItem(
SHAMapNodeType::TnTransactionNm,
makeShamapitem(kKeys[k], intToVuc(static_cast<std::uint8_t>(k)))));
EXPECT_EQ(map.getHash().asUInt256(), kHashes[k]);
map.invariants();
}
for (std::size_t k = kKeys.size(); k-- > 0;)
{
EXPECT_EQ(map.getHash().asUInt256(), kHashes[k]);
EXPECT_TRUE(map.delItem(kKeys[k]));
map.invariants();
}
EXPECT_EQ(map.getHash(), beast::kZero);
}
{
constexpr std::array kKeys{
uint256{"f22891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b99891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92881fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92791fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92691fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b91891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"292891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
};
tests::TestNodeFamily tf{j_};
SHAMap map{SHAMapType::FREE, tf};
if (!testMode.backed)
map.setUnbacked();
for (auto const& k : kKeys)
{
map.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(k, intToVuc(0)));
map.invariants();
}
int h = 7;
for (auto const& k : map)
{
EXPECT_EQ(k.key(), kKeys[h]);
--h;
}
}
}
INSTANTIATE_TEST_SUITE_P(
BackingMode,
SHAMapTest,
::testing::Values(kBackedMode, kUnbackedMode),
shamapBackingModeName);
class SHAMapPathProof : public ::testing::Test
{
protected:
beast::Journal const j_{TestSink::instance()};
};
TEST_F(SHAMapPathProof, verify_proof_path)
{
tests::TestNodeFamily tf{j_};
SHAMap map{SHAMapType::FREE, tf};
map.setUnbacked();
uint256 key;
uint256 rootHash;
std::vector<Blob> goodPath;
for (unsigned char c = 1; c < 100; ++c)
{
uint256 k(c);
map.addItem(SHAMapNodeType::TnAccountState, makeShamapitem(k, Slice{k.data(), k.size()}));
map.invariants();
auto root = map.getHash().asUInt256();
auto path = map.getProofPath(k);
if (!path)
{
ADD_FAILURE() << "Missing proof path";
return;
}
auto& proofPath = *path;
EXPECT_TRUE(map.verifyProofPath(root, k, proofPath));
if (c == 1)
{
// extra node
proofPath.insert(proofPath.begin(), proofPath.front());
EXPECT_FALSE(map.verifyProofPath(root, k, proofPath));
// wrong key
uint256 const wrongKey(c + 1);
EXPECT_FALSE(map.getProofPath(wrongKey));
}
if (c == 99)
{
key = k;
rootHash = root;
goodPath = std::move(proofPath);
}
}
// still good
EXPECT_TRUE(map.verifyProofPath(rootHash, key, goodPath));
// empty path
std::vector<Blob> badPath;
EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
// too long
badPath = goodPath;
badPath.push_back(goodPath.back());
EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
// bad node
badPath.clear();
badPath.emplace_back(100, 100);
EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
// bad node type
badPath.clear();
badPath.push_back(goodPath.front());
badPath.front().back()--; // change node type
EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
// all inner
badPath.clear();
badPath = goodPath;
badPath.erase(badPath.begin());
EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
}
} // namespace xrpl::tests

View File

@@ -0,0 +1,170 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/SHAMapHash.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <shamap/common.h>
#include <chrono>
#include <cstdint>
#include <list>
#include <utility>
#include <vector>
namespace xrpl::tests {
class SHAMapSyncTest : public ::testing::Test
{
protected:
beast::Journal const j_{TestSink::instance()};
beast::xor_shift_engine eng_;
boost::intrusive_ptr<SHAMapItem>
makeRandomAS()
{
Serializer s;
for (int d = 0; d < 3; ++d)
s.add32(randInt<std::uint32_t>(eng_));
return makeShamapitem(s.getSHA512Half(), s.slice());
}
bool
confuseMap(SHAMap& map, int count)
{
// add a bunch of random states to a map, then remove them
// map should be the same
SHAMapHash const beforeHash = map.getHash();
std::list<uint256> items;
for (int i = 0; i < count; ++i)
{
auto item = makeRandomAS();
items.push_back(item->key());
if (!map.addItem(SHAMapNodeType::TnAccountState, item))
{
ADD_FAILURE() << "Unable to add item to map";
return false;
}
}
for (auto const& item : items)
{
if (!map.delItem(item))
{
ADD_FAILURE() << "Unable to remove item from map";
return false;
}
}
if (beforeHash != map.getHash())
{
ADD_FAILURE() << "Hashes do not match " << beforeHash << " " << map.getHash();
return false;
}
return true;
}
};
TEST_F(SHAMapSyncTest, sync)
{
TestNodeFamily f{j_}, f2{j_};
SHAMap source{SHAMapType::FREE, f};
SHAMap destination{SHAMapType::FREE, f2};
int const items = 10000;
for (int i = 0; i < items; ++i)
{
source.addItem(SHAMapNodeType::TnAccountState, makeRandomAS());
if (i % 100 == 0)
source.invariants();
}
source.invariants();
ASSERT_TRUE(confuseMap(source, 500));
source.invariants();
source.setImmutable();
int count = 0;
source.visitLeaves([&count]([[maybe_unused]] auto const& item) { ++count; });
EXPECT_EQ(count, items);
std::vector<SHAMapMissingNode> missingNodes;
source.walkMap(missingNodes, 2048);
EXPECT_TRUE(missingNodes.empty());
destination.setSynching();
{
std::vector<std::pair<SHAMapNodeID, Blob>> a;
ASSERT_TRUE(source.getNodeFat(SHAMapNodeID(), a, randBool(eng_), randInt(eng_, 2)));
ASSERT_FALSE(a.empty()) << "NodeSize";
ASSERT_TRUE(
destination.addRootNode(source.getHash(), makeSlice(a[0].second), nullptr).isGood());
}
do
{
f.clock().advance(std::chrono::seconds(1));
// get the list of nodes we know we need
auto nodesMissing = destination.getMissingNodes(2048, nullptr);
if (nodesMissing.empty())
break;
// get as many nodes as possible based on this information
std::vector<std::pair<SHAMapNodeID, Blob>> b;
for (auto& it : nodesMissing)
{
// Keep failures fatal here because this loop is data-dependent.
// non-deterministic number of times and the number of tests run
// should be deterministic
if (!source.getNodeFat(it.first, b, randBool(eng_), randInt(eng_, 2)))
FAIL() << "Unable to fetch node";
}
// Keep failures fatal here because this loop is data-dependent.
// non-deterministic number of times and the number of tests run
// should be deterministic
if (b.empty())
FAIL() << "No nodes returned";
for (auto const& i : b)
{
// Keep failures fatal here because this loop is data-dependent.
// non-deterministic number of times and the number of tests run
// should be deterministic
if (!destination.addKnownNode(i.first, makeSlice(i.second), nullptr).isUseful())
FAIL() << "Known node was not useful";
}
} while (true);
destination.clearSynching();
EXPECT_TRUE(source.deepCompare(destination));
destination.invariants();
}
} // namespace xrpl::tests

View File

@@ -0,0 +1,123 @@
#pragma once
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/config/Constants.h>
#include <xrpl/nodestore/Database.h>
#include <xrpl/nodestore/DummyScheduler.h>
#include <xrpl/nodestore/Manager.h>
#include <xrpl/shamap/Family.h>
#include <xrpl/shamap/FullBelowCache.h>
#include <xrpl/shamap/TreeNodeCache.h>
#include <chrono>
#include <cstdint>
#include <memory>
#include <stdexcept>
namespace xrpl::tests {
class TestNodeFamily : public Family
{
private:
std::unique_ptr<NodeStore::Database> db_;
std::shared_ptr<FullBelowCache> fbCache_;
std::shared_ptr<TreeNodeCache> tnCache_;
TestStopwatch clock_;
NodeStore::DummyScheduler scheduler_;
beast::Journal const j_;
public:
TestNodeFamily(beast::Journal j)
: fbCache_(std::make_shared<FullBelowCache>("App family full below cache", clock_, j))
, tnCache_(
std::make_shared<TreeNodeCache>(
"App family tree node cache",
65536,
std::chrono::minutes{1},
clock_,
j))
, j_(j)
{
Section testSection;
testSection.set(Keys::kType, "memory");
testSection.set(Keys::kPath, "SHAMap_test");
db_ = NodeStore::Manager::instance().makeDatabase(
megabytes(4), scheduler_, 1, testSection, j);
}
NodeStore::Database&
db() override
{
return *db_;
}
[[nodiscard]] NodeStore::Database const&
db() const override
{
return *db_;
}
beast::Journal const&
journal() override
{
return j_;
}
std::shared_ptr<FullBelowCache>
getFullBelowCache() override
{
return fbCache_;
}
std::shared_ptr<TreeNodeCache>
getTreeNodeCache() override
{
return tnCache_;
}
void
sweep() override
{
fbCache_->sweep();
tnCache_->sweep();
}
void
missingNodeAcquireBySeq(
[[maybe_unused]] std::uint32_t refNum,
[[maybe_unused]] uint256 const& nodeHash) override
{
Throw<std::runtime_error>("missing node");
}
void
missingNodeAcquireByHash(
[[maybe_unused]] uint256 const& refHash,
[[maybe_unused]] std::uint32_t refNum) override
{
Throw<std::runtime_error>("missing node");
}
void
reset() override
{
(*fbCache_).reset();
(*tnCache_).reset();
}
TestStopwatch&
clock()
{
return clock_;
}
};
} // namespace xrpl::tests