Merge branch 'pratik/otel-phase6-statsd' into pratik/otel-phase7-native-metrics

Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
This commit is contained in:
Pratik Mankawde
2026-07-27 17:05:46 +01:00
252 changed files with 9349 additions and 7800 deletions

View File

@@ -21,18 +21,21 @@ set_target_properties(
)
# Lets test sources include the shared helpers as <helpers/...>.
target_include_directories(xrpl_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(xrpl_tests PRIVATE GTest::gtest xrpl.libxrpl)
target_link_libraries(xrpl_tests PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl)
# One source subdirectory per module. Network unit tests are currently not
# supported on Windows.
set(test_modules
basics
consensus
crypto
json
peerfinder
resource
shamap
tx
protocol_autogen
nodestore
telemetry
)
if(NOT WIN32)
@@ -58,6 +61,16 @@ foreach(module IN LISTS test_modules)
)
endforeach()
# The consensus tests use the CSF (Consensus Simulation Framework) helpers, so
# compile the CSF sources into the test binary. The consensus engine itself now
# lives in libxrpl, so no xrpld sources or include paths are needed here.
file(
GLOB_RECURSE csf_sources
CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/csf/*.cpp"
)
target_sources(xrpl_tests PRIVATE ${csf_sources})
# The test helpers and per-module test headers are not built with add_module,
# so verify them against the test binary's own compile environment.
if(verify_headers)

View File

@@ -1,5 +1,6 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/utility/Zero.h>

View File

@@ -0,0 +1,81 @@
#include <xrpl/consensus/ConsensusParms.h>
#include <csf/Peer.h>
#include <csf/PeerGroup.h>
#include <csf/Sim.h>
#include <csf/SimTime.h>
#include <csf/TrustGraph.h>
#include <csf/collectors.h>
#include <gtest/gtest.h>
#include <chrono>
#include <ios>
#include <iostream>
namespace xrpl::test {
TEST(ByzantineFailureSimTest, DISABLED_byzantine_failure_sim)
{
using namespace csf;
using namespace std::chrono;
// This test simulates a specific topology with nodes generating
// different ledgers due to a simulated byzantine failure (injecting
// an extra non-consensus transaction).
Sim sim;
ConsensusParms const parms{};
SimDuration const delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
PeerGroup a = sim.createGroup(1);
PeerGroup b = sim.createGroup(1);
PeerGroup c = sim.createGroup(1);
PeerGroup d = sim.createGroup(1);
PeerGroup e = sim.createGroup(1);
PeerGroup f = sim.createGroup(1);
PeerGroup g = sim.createGroup(1);
a.trustAndConnect(a + b + c + g, delay);
b.trustAndConnect(b + a + c + d + e, delay);
c.trustAndConnect(c + a + b + d + e, delay);
d.trustAndConnect(d + b + c + e + f, delay);
e.trustAndConnect(e + b + c + d + f, delay);
f.trustAndConnect(f + d + e + g, delay);
g.trustAndConnect(g + a + f, delay);
PeerGroup const network = a + b + c + d + e + f + g;
StreamCollector sc{std::cout};
sim.collectors.add(sc);
for (TrustGraph<Peer*>::ForkInfo const& fi : sim.trustGraph.forkablePairs(0.8))
{
std::cout << "Can fork " << PeerGroup{fi.unlA} << " "
<< " " << PeerGroup{fi.unlB} << " overlap " << fi.overlap << " required "
<< fi.required << "\n";
};
// set prior state
sim.run(1);
PeerGroup byzantineNodes = a + b + c + g;
// All peers see some TX 0
for (Peer* peer : network)
{
peer->submit(Tx{0});
// Peers 0,1,2,6 will close the next ledger differently by injecting
// a non-consensus approved transaction
if (byzantineNodes.contains(peer))
{
peer->txInjections.emplace(peer->lastClosedLedger.seq(), Tx{42});
}
}
sim.run(4);
std::cout << "Branches: " << sim.branches() << "\n";
std::cout << "Fully synchronized: " << std::boolalpha << sim.synchronized() << "\n";
// Not tessting anything currently.
SUCCEED();
}
} // namespace xrpl::test

View File

@@ -0,0 +1,81 @@
#include <xrpl/consensus/CensorshipDetector.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <utility>
#include <vector>
namespace xrpl::test {
namespace {
void
runRound(
CensorshipDetector<int, int>& cdet,
int round,
std::vector<int> proposed,
std::vector<int> accepted,
std::vector<int> remain,
std::vector<int> remove)
{
// Begin tracking what we're proposing this round
CensorshipDetector<int, int>::TxIDSeqVec proposal;
for (auto const& i : proposed)
proposal.emplace_back(i, round);
cdet.propose(std::move(proposal));
// Finalize the round, by processing what we accepted; then
// remove anything that needs to be removed and ensure that
// what remains is correct.
cdet.check(std::move(accepted), [&remove, &remain](auto id, auto seq) {
// If the item is supposed to be removed from the censorship
// detector internal tracker manually, do it now:
if (std::ranges::find(remove, id) != remove.end())
return true;
// If the item is supposed to still remain in the censorship
// detector internal tracker; remove it from the vector.
auto it = std::ranges::find(remain, id);
if (it != remain.end())
remain.erase(it);
return false;
});
// On entry, this set contained all the elements that should be tracked
// by the detector after we process this round. We removed all the items
// that actually were in the tracker, so this should now be empty:
EXPECT_TRUE(remain.empty());
}
} // namespace
TEST(CensorshipDetectorTest, censorship_detector)
{
SCOPED_TRACE("Censorship Detector");
CensorshipDetector<int, int> cdet;
int round = 0;
// proposed accepted remain remove
runRound(cdet, ++round, {}, {}, {}, {});
runRound(cdet, ++round, {10, 11, 12, 13}, {11, 2}, {10, 13}, {});
runRound(cdet, ++round, {10, 13, 14, 15}, {14}, {10, 13, 15}, {});
runRound(cdet, ++round, {10, 13, 15, 16}, {15, 16}, {10, 13}, {});
runRound(cdet, ++round, {10, 13}, {17, 18}, {10, 13}, {});
runRound(cdet, ++round, {10, 19}, {}, {10, 19}, {});
runRound(cdet, ++round, {10, 19, 20}, {20}, {10}, {19});
runRound(cdet, ++round, {21}, {21}, {}, {});
runRound(cdet, ++round, {}, {22}, {}, {});
runRound(cdet, ++round, {23, 24, 25, 26}, {25, 27}, {23, 26}, {24});
runRound(cdet, ++round, {23, 26, 28}, {26, 28}, {23}, {});
for (int i = 0; i != 10; ++i)
runRound(cdet, ++round, {23}, {}, {23}, {});
runRound(cdet, ++round, {23, 29}, {29}, {23}, {});
runRound(cdet, ++round, {30, 31}, {31}, {30}, {});
runRound(cdet, ++round, {30}, {30}, {}, {});
runRound(cdet, ++round, {}, {}, {}, {});
}
} // namespace xrpl::test

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,252 @@
#include <csf/PeerGroup.h>
#include <csf/Sim.h>
#include <csf/collectors.h>
#include <csf/random.h>
#include <csf/submitters.h>
#include <csf/timers.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <fstream>
#include <iomanip>
#include <ios>
#include <iostream>
#include <ostream>
#include <random>
#include <sstream>
#include <string>
#include <vector>
namespace xrpl::test {
namespace {
[[nodiscard]] std::string const&
arg()
{
static std::string const kEMPTY;
return kEMPTY;
}
void
completeTrustCompleteConnectFixedDelay(
std::size_t numPeers,
std::chrono::milliseconds delay = std::chrono::milliseconds(200),
bool printHeaders = false)
{
using namespace csf;
using namespace std::chrono;
// Initialize persistent collector logs specific to this method
std::string const prefix =
"DistributedValidators_"
"completeTrustCompleteConnectFixedDelay";
std::fstream txLog(prefix + "_tx.csv", std::ofstream::app),
ledgerLog(prefix + "_ledger.csv", std::ofstream::app);
// title
std::cout << prefix << "(" << numPeers << "," << delay.count() << ")" << std::endl;
// number of peers, UNLs, connections
EXPECT_TRUE(numPeers >= 1);
Sim sim;
PeerGroup peers = sim.createGroup(numPeers);
// complete trust graph
peers.trust(peers);
// complete connect graph with fixed delay
peers.connect(peers, delay);
// Initialize collectors to track statistics to report
TxCollector txCollector;
LedgerCollector ledgerCollector;
auto colls = makeCollectors(txCollector, ledgerCollector);
sim.collectors.add(colls);
// Initial round to set prior state
sim.run(1);
// Run for 10 minutes, submitting 100 tx/second
std::chrono::nanoseconds const simDuration = 10min;
std::chrono::nanoseconds const quiet = 10s;
Rate const rate{.count = 100, .duration = 1000ms};
// Initialize timers
HeartbeatTimer heart(sim.scheduler);
// txs, start/stop/step, target
auto peerSelector =
makeSelector(peers.begin(), peers.end(), std::vector<double>(numPeers, 1.), sim.rng);
auto txSubmitter = makeSubmitter(
ConstantDistribution{rate.inv()},
sim.scheduler.now() + quiet,
sim.scheduler.now() + simDuration - quiet,
peerSelector,
sim.scheduler,
sim.rng);
// run simulation for given duration
heart.start();
sim.run(simDuration);
// EXPECT_TRUE(sim.branches() == 1);
// EXPECT_TRUE(sim.synchronized());
std::cout << std::right;
std::cout << "| Peers: " << std::setw(2) << peers.size();
std::cout << " | Duration: " << std::setw(6) << duration_cast<milliseconds>(simDuration).count()
<< " ms";
std::cout << " | Branches: " << std::setw(1) << sim.branches();
std::cout << " | Synchronized: " << std::setw(1) << (sim.synchronized() ? "Y" : "N");
std::cout << " |" << std::endl;
txCollector.report(simDuration, std::cout, true);
ledgerCollector.report(simDuration, std::cout, false);
std::string const tag = std::to_string(numPeers);
txCollector.csv(simDuration, txLog, tag, printHeaders);
ledgerCollector.csv(simDuration, ledgerLog, tag, printHeaders);
std::cout << std::endl;
}
void
completeTrustScaleFreeConnectFixedDelay(
std::size_t numPeers,
std::chrono::milliseconds delay = std::chrono::milliseconds(200),
bool printHeaders = false)
{
using namespace csf;
using namespace std::chrono;
// Initialize persistent collector logs specific to this method
std::string const prefix =
"DistributedValidators__"
"completeTrustScaleFreeConnectFixedDelay";
std::fstream txLog(prefix + "_tx.csv", std::ofstream::app),
ledgerLog(prefix + "_ledger.csv", std::ofstream::app);
// title
std::cout << prefix << "(" << numPeers << "," << delay.count() << ")" << std::endl;
// number of peers, UNLs, connections
int const numCNLs = std::max(int(1.00 * numPeers), 1);
int const minCNLSize = std::max(int(0.25 * numCNLs), 1);
int const maxCNLSize = std::max(int(0.50 * numCNLs), 1);
EXPECT_TRUE(numPeers >= 1);
EXPECT_TRUE(numCNLs >= 1);
EXPECT_TRUE(1 <= minCNLSize && minCNLSize <= maxCNLSize && maxCNLSize <= numPeers);
Sim sim;
PeerGroup peers = sim.createGroup(numPeers);
// complete trust graph
peers.trust(peers);
// scale-free connect graph with fixed delay
std::vector<double> const ranks = sample(peers.size(), PowerLawDistribution{1, 3}, sim.rng);
randomRankedConnect(
peers,
ranks,
numCNLs,
std::uniform_int_distribution<>{minCNLSize, maxCNLSize},
sim.rng,
delay);
// Initialize collectors to track statistics to report
TxCollector txCollector;
LedgerCollector ledgerCollector;
auto colls = makeCollectors(txCollector, ledgerCollector);
sim.collectors.add(colls);
// Initial round to set prior state
sim.run(1);
// Run for 10 minutes, submitting 100 tx/second
std::chrono::nanoseconds const simDuration = 10min;
std::chrono::nanoseconds const quiet = 10s;
Rate const rate{.count = 100, .duration = 1000ms};
// Initialize timers
HeartbeatTimer heart(sim.scheduler);
// txs, start/stop/step, target
auto peerSelector =
makeSelector(peers.begin(), peers.end(), std::vector<double>(numPeers, 1.), sim.rng);
auto txSubmitter = makeSubmitter(
ConstantDistribution{rate.inv()},
sim.scheduler.now() + quiet,
sim.scheduler.now() + simDuration - quiet,
peerSelector,
sim.scheduler,
sim.rng);
// run simulation for given duration
heart.start();
sim.run(simDuration);
// EXPECT_TRUE(sim.branches() == 1);
// EXPECT_TRUE(sim.synchronized());
std::cout << std::right;
std::cout << "| Peers: " << std::setw(2) << peers.size();
std::cout << " | Duration: " << std::setw(6) << duration_cast<milliseconds>(simDuration).count()
<< " ms";
std::cout << " | Branches: " << std::setw(1) << sim.branches();
std::cout << " | Synchronized: " << std::setw(1) << (sim.synchronized() ? "Y" : "N");
std::cout << " |" << std::endl;
txCollector.report(simDuration, std::cout, true);
ledgerCollector.report(simDuration, std::cout, false);
std::string const tag = std::to_string(numPeers);
txCollector.csv(simDuration, txLog, tag, printHeaders);
ledgerCollector.csv(simDuration, ledgerLog, tag, printHeaders);
std::cout << std::endl;
}
} // namespace
// In progress simulations for diversifying and distributing validators
TEST(DistributedValidatorsTest, DISABLED_distributed_validators)
{
std::string const defaultArgs = "5 200";
std::string const args = arg().empty() ? defaultArgs : arg();
std::stringstream argStream(args);
int maxNumValidators = 0;
int delayCount(200);
argStream >> maxNumValidators;
argStream >> delayCount;
std::chrono::milliseconds const delay(delayCount);
std::cout << "DistributedValidators: 1 to " << maxNumValidators << " Peers" << std::endl;
// Simulate with N = 1 to N
// - complete trust graph is complete
// - complete network connectivity
// - fixed delay for network links
completeTrustCompleteConnectFixedDelay(1, delay, true);
for (int i = 2; i <= maxNumValidators; i++)
{
completeTrustCompleteConnectFixedDelay(i, delay);
}
// Simulate with N = 1 to N
// - complete trust graph is complete
// - scale-free network connectivity
// - fixed delay for network links
completeTrustScaleFreeConnectFixedDelay(1, delay, true);
for (int i = 2; i <= maxNumValidators; i++)
{
completeTrustScaleFreeConnectFixedDelay(i, delay);
}
}
} // namespace xrpl::test

View File

@@ -0,0 +1,105 @@
#include <xrpl/ledger/LedgerTiming.h>
#include <xrpl/basics/chrono.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstdint>
#include <utility>
namespace xrpl::test {
TEST(LedgerTimingTest, get_next_ledger_time_resolution)
{
// helper to iteratively call into getNextLedgerTimeResolution
struct TestRes
{
std::uint32_t decrease = 0;
std::uint32_t equal = 0;
std::uint32_t increase = 0;
static TestRes
run(bool previousAgree, std::uint32_t rounds)
{
TestRes res;
auto closeResolution = kLedgerDefaultTimeResolution;
auto nextCloseResolution = closeResolution;
std::uint32_t round = 0;
do
{
nextCloseResolution =
getNextLedgerTimeResolution(closeResolution, previousAgree, ++round);
if (nextCloseResolution < closeResolution)
{
++res.decrease;
}
else if (nextCloseResolution > closeResolution)
{
++res.increase;
}
else
{
++res.equal;
}
std::swap(nextCloseResolution, closeResolution);
} while (round < rounds);
return res;
}
};
// If we never agree on close time, only can increase resolution
// until hit the max
auto decreases = TestRes::run(false, 10);
EXPECT_TRUE(decreases.increase == 3);
EXPECT_TRUE(decreases.decrease == 0);
EXPECT_TRUE(decreases.equal == 7);
// If we always agree on close time, only can decrease resolution
// until hit the min
auto increases = TestRes::run(false, 100);
EXPECT_TRUE(increases.increase == 3);
EXPECT_TRUE(increases.decrease == 0);
EXPECT_TRUE(increases.equal == 97);
}
TEST(LedgerTimingTest, round_close_time)
{
using namespace std::chrono_literals;
// A closeTime equal to the epoch is not modified
using tp = NetClock::time_point;
tp const def;
EXPECT_TRUE(def == roundCloseTime(def, 30s));
// Otherwise, the closeTime is rounded to the nearest
// rounding up on ties
EXPECT_TRUE(tp{0s} == roundCloseTime(tp{29s}, 60s));
EXPECT_TRUE(tp{30s} == roundCloseTime(tp{30s}, 1s));
EXPECT_TRUE(tp{60s} == roundCloseTime(tp{31s}, 60s));
EXPECT_TRUE(tp{60s} == roundCloseTime(tp{30s}, 60s));
EXPECT_TRUE(tp{60s} == roundCloseTime(tp{59s}, 60s));
EXPECT_TRUE(tp{60s} == roundCloseTime(tp{60s}, 60s));
EXPECT_TRUE(tp{60s} == roundCloseTime(tp{61s}, 60s));
}
TEST(LedgerTimingTest, eff_close_time)
{
using namespace std::chrono_literals;
using tp = NetClock::time_point;
tp close = effCloseTime(tp{10s}, 30s, tp{0s});
EXPECT_TRUE(close == tp{1s});
close = effCloseTime(tp{16s}, 30s, tp{0s});
EXPECT_TRUE(close == tp{30s});
close = effCloseTime(tp{16s}, 30s, tp{30s});
EXPECT_TRUE(close == tp{31s});
close = effCloseTime(tp{16s}, 30s, tp{60s});
EXPECT_TRUE(close == tp{61s});
close = effCloseTime(tp{31s}, 30s, tp{0s});
EXPECT_TRUE(close == tp{30s});
}
} // namespace xrpl::test

View File

@@ -0,0 +1,693 @@
#include <xrpl/consensus/LedgerTrie.h>
#include <csf/ledgers.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <optional>
#include <random>
namespace xrpl::test {
TEST(LedgerTrieTest, insert)
{
using namespace csf;
// Single entry by itself
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 1);
t.insert(h["abc"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 2);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 2);
}
// Suffix of existing (extending tree)
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
EXPECT_TRUE(t.checkInvariants());
// extend with no siblings
t.insert(h["abcd"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 2);
EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcd"]) == 1);
// extend with existing sibling
t.insert(h["abce"]);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 3);
EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.tipSupport(h["abce"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abce"]) == 1);
}
// uncommitted of existing node
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abcd"]);
EXPECT_TRUE(t.checkInvariants());
// uncommitted with no siblings
t.insert(h["abcdf"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcd"]) == 2);
EXPECT_TRUE(t.tipSupport(h["abcdf"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcdf"]) == 1);
// uncommitted with existing child
t.insert(h["abc"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 3);
EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcd"]) == 2);
EXPECT_TRUE(t.tipSupport(h["abcdf"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcdf"]) == 1);
}
// Suffix + uncommitted of existing node
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abcd"]);
EXPECT_TRUE(t.checkInvariants());
t.insert(h["abce"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 0);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 2);
EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.tipSupport(h["abce"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abce"]) == 1);
}
// Suffix + uncommitted with existing child
{
// abcd : abcde, abcf
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abcd"]);
EXPECT_TRUE(t.checkInvariants());
t.insert(h["abcde"]);
EXPECT_TRUE(t.checkInvariants());
t.insert(h["abcf"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 0);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 3);
EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcd"]) == 2);
EXPECT_TRUE(t.tipSupport(h["abcf"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcf"]) == 1);
EXPECT_TRUE(t.tipSupport(h["abcde"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcde"]) == 1);
}
// Multiple counts
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["ab"], 4);
EXPECT_TRUE(t.tipSupport(h["ab"]) == 4);
EXPECT_TRUE(t.branchSupport(h["ab"]) == 4);
EXPECT_TRUE(t.tipSupport(h["a"]) == 0);
EXPECT_TRUE(t.branchSupport(h["a"]) == 4);
t.insert(h["abc"], 2);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 2);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 2);
EXPECT_TRUE(t.tipSupport(h["ab"]) == 4);
EXPECT_TRUE(t.branchSupport(h["ab"]) == 6);
EXPECT_TRUE(t.tipSupport(h["a"]) == 0);
EXPECT_TRUE(t.branchSupport(h["a"]) == 6);
}
}
TEST(LedgerTrieTest, remove)
{
using namespace csf;
// Not in trie
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
EXPECT_TRUE(!t.remove(h["ab"]));
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(!t.remove(h["a"]));
EXPECT_TRUE(t.checkInvariants());
}
// In trie but with 0 tip support
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abcd"]);
t.insert(h["abce"]);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 0);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 2);
EXPECT_TRUE(!t.remove(h["abc"]));
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 0);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 2);
}
// In trie with > 1 tip support
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"], 2);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 2);
EXPECT_TRUE(t.remove(h["abc"]));
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
t.insert(h["abc"], 1);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 2);
EXPECT_TRUE(t.remove(h["abc"], 2));
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 0);
t.insert(h["abc"], 3);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 3);
EXPECT_TRUE(t.remove(h["abc"], 300));
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 0);
}
// In trie with = 1 tip support, no children
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["ab"]);
t.insert(h["abc"]);
EXPECT_TRUE(t.tipSupport(h["ab"]) == 1);
EXPECT_TRUE(t.branchSupport(h["ab"]) == 2);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 1);
EXPECT_TRUE(t.remove(h["abc"]));
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["ab"]) == 1);
EXPECT_TRUE(t.branchSupport(h["ab"]) == 1);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 0);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 0);
}
// In trie with = 1 tip support, 1 child
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["ab"]);
t.insert(h["abc"]);
t.insert(h["abcd"]);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 2);
EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.remove(h["abc"]));
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 0);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 1);
EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcd"]) == 1);
}
// In trie with = 1 tip support, > 1 children
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["ab"]);
t.insert(h["abc"]);
t.insert(h["abcd"]);
t.insert(h["abce"]);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 3);
EXPECT_TRUE(t.remove(h["abc"]));
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 0);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 2);
}
// In trie with = 1 tip support, parent compaction
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["ab"]);
t.insert(h["abc"]);
t.insert(h["abd"]);
EXPECT_TRUE(t.checkInvariants());
t.remove(h["ab"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.tipSupport(h["abd"]) == 1);
EXPECT_TRUE(t.tipSupport(h["ab"]) == 0);
EXPECT_TRUE(t.branchSupport(h["ab"]) == 2);
t.remove(h["abd"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.branchSupport(h["ab"]) == 1);
}
}
TEST(LedgerTrieTest, empty)
{
using namespace csf;
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
EXPECT_TRUE(t.empty());
Ledger const genesis = h[""];
t.insert(genesis);
EXPECT_TRUE(!t.empty());
t.remove(genesis);
EXPECT_TRUE(t.empty());
t.insert(h["abc"]);
EXPECT_TRUE(!t.empty());
t.remove(h["abc"]);
EXPECT_TRUE(t.empty());
}
TEST(LedgerTrieTest, support)
{
using namespace csf;
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
EXPECT_TRUE(t.tipSupport(h["a"]) == 0);
EXPECT_TRUE(t.tipSupport(h["axy"]) == 0);
EXPECT_TRUE(t.branchSupport(h["a"]) == 0);
EXPECT_TRUE(t.branchSupport(h["axy"]) == 0);
t.insert(h["abc"]);
EXPECT_TRUE(t.tipSupport(h["a"]) == 0);
EXPECT_TRUE(t.tipSupport(h["ab"]) == 0);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.tipSupport(h["abcd"]) == 0);
EXPECT_TRUE(t.branchSupport(h["a"]) == 1);
EXPECT_TRUE(t.branchSupport(h["ab"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abcd"]) == 0);
t.insert(h["abe"]);
EXPECT_TRUE(t.tipSupport(h["a"]) == 0);
EXPECT_TRUE(t.tipSupport(h["ab"]) == 0);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 1);
EXPECT_TRUE(t.tipSupport(h["abe"]) == 1);
EXPECT_TRUE(t.branchSupport(h["a"]) == 2);
EXPECT_TRUE(t.branchSupport(h["ab"]) == 2);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abe"]) == 1);
t.remove(h["abc"]);
EXPECT_TRUE(t.tipSupport(h["a"]) == 0);
EXPECT_TRUE(t.tipSupport(h["ab"]) == 0);
EXPECT_TRUE(t.tipSupport(h["abc"]) == 0);
EXPECT_TRUE(t.tipSupport(h["abe"]) == 1);
EXPECT_TRUE(t.branchSupport(h["a"]) == 1);
EXPECT_TRUE(t.branchSupport(h["ab"]) == 1);
EXPECT_TRUE(t.branchSupport(h["abc"]) == 0);
EXPECT_TRUE(t.branchSupport(h["abe"]) == 1);
}
TEST(LedgerTrieTest, get_preferred)
{
using namespace csf;
using Seq = Ledger::Seq;
// Empty
{
LedgerTrie<Ledger> const t;
EXPECT_TRUE(t.getPreferred(Seq{0}) == std::nullopt);
EXPECT_TRUE(t.getPreferred(Seq{2}) == std::nullopt);
}
// Genesis support is NOT empty
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
Ledger const genesis = h[""];
t.insert(genesis);
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{0})->id == genesis.id());
EXPECT_TRUE(t.remove(genesis));
EXPECT_TRUE(t.getPreferred(Seq{0}) == std::nullopt);
EXPECT_TRUE(!t.remove(genesis));
}
// Single node no children
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id());
}
// Single node smaller child support
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
t.insert(h["abcd"]);
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id());
}
// Single node larger child
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
t.insert(h["abcd"], 2);
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abcd"].id());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abcd"].id());
}
// Single node smaller children support
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
t.insert(h["abcd"]);
t.insert(h["abce"]);
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id());
t.insert(h["abc"]);
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id());
// NOLINTEND(bugprone-unchecked-optional-access)
}
// Single node larger children
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
t.insert(h["abcd"], 2);
t.insert(h["abce"]);
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id());
t.insert(h["abcd"]);
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abcd"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abcd"].id());
// NOLINTEND(bugprone-unchecked-optional-access)
}
// Tie-breaker by id
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abcd"], 2);
t.insert(h["abce"], 2);
EXPECT_TRUE(h["abce"].id() > h["abcd"].id());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abce"].id());
t.insert(h["abcd"]);
EXPECT_TRUE(h["abce"].id() > h["abcd"].id());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abcd"].id());
}
// Tie-breaker not needed
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
t.insert(h["abcd"]);
t.insert(h["abce"], 2);
// abce only has a margin of 1, but it owns the tie-breaker
EXPECT_TRUE(h["abce"].id() > h["abcd"].id());
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abce"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abce"].id());
// Switch support from abce to abcd, tie-breaker now needed
t.remove(h["abce"]);
t.insert(h["abcd"]);
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id());
// NOLINTEND(bugprone-unchecked-optional-access)
}
// Single node larger grand child
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
t.insert(h["abcd"], 2);
t.insert(h["abcde"], 4);
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abcde"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abcde"].id());
EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["abcde"].id());
// NOLINTEND(bugprone-unchecked-optional-access)
}
// Too much uncommitted support from competing branches
{
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["abc"]);
t.insert(h["abcde"], 2);
t.insert(h["abcfg"], 2);
// 'de' and 'fg' are tied without 'abc' vote
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id());
EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["abc"].id());
t.remove(h["abc"]);
t.insert(h["abcd"]);
// 'de' branch has 3 votes to 2, so earlier sequences see it as preferred
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abcde"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abcde"].id());
// However, if you validated a ledger with Seq 5, potentially on
// a different branch, you do not yet know if they chose abcd
// or abcf because of you, so abc remains preferred
EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["abc"].id());
// NOLINTEND(bugprone-unchecked-optional-access)
}
// Changing largestSeq perspective changes preferred branch
{
/**
* Build the tree below with initial tip support annotated
* A
* / \
* B(1) C(1)
* / | |
* H D F(1)
* |
* E(2)
* |
* G
*/
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
t.insert(h["ab"]);
t.insert(h["ac"]);
t.insert(h["acf"]);
t.insert(h["abde"], 2);
// B has more branch support
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{1})->id == h["ab"].id());
EXPECT_TRUE(t.getPreferred(Seq{2})->id == h["ab"].id());
// But if you last validated D,F or E, you do not yet know
// if someone used that validation to commit to B or C
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["a"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["a"].id());
// NOLINTEND(bugprone-unchecked-optional-access)
/**
* One of E advancing to G doesn't change anything
* A
* / \
* B(1) C(1)
* / | |
* H D F(1)
* |
* E(1)
* |
* G(1)
*/
t.remove(h["abde"]);
t.insert(h["abdeg"]);
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{1})->id == h["ab"].id());
EXPECT_TRUE(t.getPreferred(Seq{2})->id == h["ab"].id());
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["a"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["a"].id());
EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["a"].id());
// NOLINTEND(bugprone-unchecked-optional-access)
/**
* C advancing to H does advance the seq 3 preferred ledger
* A
* / \
* B(1) C
* / | |
* H(1)D F(1)
* |
* E(1)
* |
* G(1)
*/
t.remove(h["ac"]);
t.insert(h["abh"]);
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{1})->id == h["ab"].id());
EXPECT_TRUE(t.getPreferred(Seq{2})->id == h["ab"].id());
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["ab"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["a"].id());
EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["a"].id());
// NOLINTEND(bugprone-unchecked-optional-access)
/**
* F advancing to E also moves the preferred ledger forward
* A
* / \
* B(1) C
* / | |
* H(1)D F
* |
* E(2)
* |
* G(1)
*/
t.remove(h["acf"]);
t.insert(h["abde"]);
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(t.getPreferred(Seq{1})->id == h["abde"].id());
EXPECT_TRUE(t.getPreferred(Seq{2})->id == h["abde"].id());
EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abde"].id());
EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["ab"].id());
EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["ab"].id());
// NOLINTEND(bugprone-unchecked-optional-access)
}
}
TEST(LedgerTrieTest, root_related)
{
using namespace csf;
// Since the root is a special node that breaks the no-single child
// invariant, do some tests that exercise it.
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
EXPECT_TRUE(!t.remove(h[""]));
EXPECT_TRUE(t.branchSupport(h[""]) == 0);
EXPECT_TRUE(t.tipSupport(h[""]) == 0);
t.insert(h["a"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.branchSupport(h[""]) == 1);
EXPECT_TRUE(t.tipSupport(h[""]) == 0);
t.insert(h["e"]);
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.branchSupport(h[""]) == 2);
EXPECT_TRUE(t.tipSupport(h[""]) == 0);
EXPECT_TRUE(t.remove(h["e"]));
EXPECT_TRUE(t.checkInvariants());
EXPECT_TRUE(t.branchSupport(h[""]) == 1);
EXPECT_TRUE(t.tipSupport(h[""]) == 0);
}
TEST(LedgerTrieTest, stress)
{
using namespace csf;
LedgerTrie<Ledger> t;
LedgerHistoryHelper h;
// Test quasi-randomly add/remove supporting for different ledgers
// from a branching history.
// Ledgers have sequence 1,2,3,4
std::uint32_t const depthConst = 4;
// Each ledger has 4 possible children
std::uint32_t const width = 4;
std::uint32_t const iterations = 10000;
// Use explicit seed to have same results for CI
// NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test
std::mt19937 gen{42};
std::uniform_int_distribution<> depthDist(0, depthConst - 1);
std::uniform_int_distribution<> widthDist(0, width - 1);
std::uniform_int_distribution<> flip(0, 1);
for (std::uint32_t i = 0; i < iterations; ++i)
{
// pick a random ledger history
std::string curr;
char const depth = depthDist(gen);
char offset = 0;
for (char d = 0; d < depth; ++d)
{
char const a = offset + widthDist(gen);
curr += a;
offset = (a + 1) * width;
}
// 50-50 to add remove
if (flip(gen) == 0)
{
t.insert(h[curr]);
}
else
{
t.remove(h[curr]);
}
EXPECT_TRUE(t.checkInvariants());
if (!(t.checkInvariants()))
return;
}
}
} // namespace xrpl::test

View File

@@ -0,0 +1,100 @@
#include <xrpl/consensus/ConsensusParms.h>
#include <csf/PeerGroup.h>
#include <csf/Sim.h>
#include <csf/collectors.h>
#include <csf/random.h>
#include <csf/submitters.h>
#include <csf/timers.h>
#include <gtest/gtest.h>
#include <chrono>
#include <iostream>
#include <ostream>
#include <random>
#include <vector>
namespace xrpl::test {
TEST(ScaleFreeSimTest, DISABLED_scale_free_sim)
{
using namespace std::chrono;
using namespace csf;
std::ostream& log = std::cout;
// Generate a quasi-random scale free network and simulate consensus
// as we vary transaction submission rates
int const n = 100; // Peers
int const numUNLs = 15; // UNL lists
int const minUNLSize = n / 4, maxUNLSize = n / 2;
ConsensusParms const parms{};
Sim sim;
PeerGroup network = sim.createGroup(n);
// generate trust ranks
std::vector<double> const ranks = sample(network.size(), PowerLawDistribution{1, 3}, sim.rng);
// generate scale-free trust graph
randomRankedTrust(
network, ranks, numUNLs, std::uniform_int_distribution<>{minUNLSize, maxUNLSize}, sim.rng);
// nodes with a trust line in either direction are network-connected
network.connectFromTrust(round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
// Initialize collectors to track statistics to report
TxCollector txCollector;
LedgerCollector ledgerCollector;
auto colls = makeCollectors(txCollector, ledgerCollector);
sim.collectors.add(colls);
// Initial round to set prior state
sim.run(1);
// Initialize timers
HeartbeatTimer heart(sim.scheduler, seconds(10s));
// Run for 10 minutes, submitting 100 tx/second
std::chrono::nanoseconds const simDuration = 10min;
std::chrono::nanoseconds const quiet = 10s;
Rate const rate{.count = 100, .duration = 1000ms};
// txs, start/stop/step, target
auto peerSelector = makeSelector(network.begin(), network.end(), ranks, sim.rng);
auto txSubmitter = makeSubmitter(
ConstantDistribution{rate.inv()},
sim.scheduler.now() + quiet,
sim.scheduler.now() + (simDuration - quiet),
peerSelector,
sim.scheduler,
sim.rng);
// run simulation for given duration
heart.start();
sim.run(simDuration);
EXPECT_TRUE(sim.branches() == 1);
EXPECT_TRUE(sim.synchronized());
// TODO: Clean up this formatting mess!!
log << "Peers: " << network.size() << std::endl;
log << "Simulated Duration: " << duration_cast<milliseconds>(simDuration).count() << " ms"
<< std::endl;
log << "Branches: " << sim.branches() << std::endl;
log << "Synchronized: " << (sim.synchronized() ? "Y" : "N") << std::endl;
log << std::endl;
txCollector.report(simDuration, log);
ledgerCollector.report(simDuration, log);
// Print summary?
// # forks? # of LCLs?
// # peers
// # tx submitted
// # ledgers/sec etc.?
}
} // namespace xrpl::test

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,122 @@
#include <csf/BasicNetwork.h>
#include <csf/Scheduler.h>
#include <gtest/gtest.h>
#include <set>
#include <vector>
namespace xrpl::test {
namespace {
struct Peer
{
int id;
std::set<int> set;
Peer(Peer const&) = default;
Peer(Peer&&) = default;
explicit Peer(int id) : id(id)
{
}
template <class Net>
void
start(csf::Scheduler& scheduler, Net& net)
{
using namespace std::chrono_literals;
auto t = scheduler.in(1s, [&] { set.insert(0); });
if (id == 0)
{
for (auto const link : net.links(this))
{
net.send(this, link.target, [&, to = link.target] { to->receive(net, this, 1); });
}
}
else
{
scheduler.cancel(t);
}
}
template <class Net>
void
receive(Net& net, Peer* from, int m)
{
set.insert(m);
++m;
if (m < 5)
{
for (auto const link : net.links(this))
{
net.send(this, link.target, [&, mm = m, to = link.target] {
to->receive(net, this, mm);
});
}
}
}
};
} // namespace
TEST(BasicNetworkTest, network)
{
using namespace std::chrono_literals;
std::vector<Peer> pv;
pv.emplace_back(0);
pv.emplace_back(1);
pv.emplace_back(2);
csf::Scheduler scheduler;
csf::BasicNetwork<Peer*> net(scheduler);
EXPECT_TRUE(!net.connect(&pv[0], &pv[0]));
EXPECT_TRUE(net.connect(&pv[0], &pv[1], 1s));
EXPECT_TRUE(net.connect(&pv[1], &pv[2], 1s));
EXPECT_TRUE(!net.connect(&pv[0], &pv[1]));
for (auto& peer : pv)
peer.start(scheduler, net);
EXPECT_TRUE(scheduler.stepFor(0s));
EXPECT_TRUE(scheduler.stepFor(1s));
EXPECT_TRUE(scheduler.step());
EXPECT_TRUE(!scheduler.step());
EXPECT_TRUE(!scheduler.stepFor(1s));
net.send(&pv[0], &pv[1], [] {});
net.send(&pv[1], &pv[0], [] {});
EXPECT_TRUE(net.disconnect(&pv[0], &pv[1]));
EXPECT_TRUE(!net.disconnect(&pv[0], &pv[1]));
for (;;)
{
auto const links = net.links(&pv[1]);
if (links.empty())
break;
EXPECT_TRUE(net.disconnect(&pv[1], links[0].target));
}
EXPECT_TRUE(pv[0].set == std::set<int>({0, 2, 4}));
EXPECT_TRUE(pv[1].set == std::set<int>({1, 3}));
EXPECT_TRUE(pv[2].set == std::set<int>({2, 4}));
}
TEST(BasicNetworkTest, disconnect)
{
using namespace std::chrono_literals;
csf::Scheduler scheduler;
csf::BasicNetwork<int> net(scheduler);
EXPECT_TRUE(net.connect(0, 1, 1s));
EXPECT_TRUE(net.connect(0, 2, 2s));
std::set<int> delivered;
net.send(0, 1, [&]() { delivered.insert(1); });
net.send(0, 2, [&]() { delivered.insert(2); });
scheduler.in(1000ms, [&]() { EXPECT_TRUE(net.disconnect(0, 2)); });
scheduler.in(1100ms, [&]() { EXPECT_TRUE(net.connect(0, 2)); });
scheduler.step();
// only the first message is delivered because the disconnect at 1 s
// purges all pending messages from 0 to 2
EXPECT_TRUE(delivered == std::set<int>({1}));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,232 @@
#pragma once
#include <csf/Digraph.h>
#include <csf/Scheduler.h>
#include <cassert>
namespace xrpl::test::csf {
/**
* Peer to peer network simulator.
*
* The network is formed from a set of Peer objects representing
* vertices and configurable connections representing edges.
* The caller is responsible for creating the Peer objects ahead
* of time.
*
* Peer objects cannot be destroyed once the BasicNetwork is
* constructed. To handle peers going online and offline,
* callers can simply disconnect all links and reconnect them
* later. Connections are directed, one end is the inbound
* Peer and the other is the outbound Peer.
*
* Peers may send messages along their connections. To simulate
* the effects of latency, these messages can be delayed by a
* configurable duration set when the link is established.
* Messages always arrive in the order they were sent on a
* particular connection.
*
* A message is modeled using a lambda function. The caller
* provides the code to execute upon delivery of the message.
* If a Peer is disconnected, all messages pending delivery
* at either end of the connection will not be delivered.
*
* When creating the Peer set, the caller needs to provide a
* Scheduler object for managing the timing and delivery
* of messages. After constructing the network, and establishing
* connections, the caller uses the scheduler's step* functions
* to drive messages through the network.
*
* The graph of peers and connections is internally represented
* using Digraph<Peer,BasicNetwork::LinkType>. Clients have
* const access to that graph to perform additional operations not
* directly provided by BasicNetwork.
*
* Peer Requirements:
*
* Peer should be a lightweight type, cheap to copy
* and/or move. A good candidate is a simple pointer to
* the underlying user defined type in the simulation.
*
* Expression Type Requirements
* ---------- ---- ------------
* P Peer
* u, v Values of type P
* P u(v) CopyConstructible
* u.~P() Destructible
* u == v bool EqualityComparable
* u < v bool LessThanComparable
* std::hash<P> class std::hash is defined for P
* ! u bool true if u is not-a-peer
*/
template <class Peer>
class BasicNetwork
{
using peer_type = Peer;
using clock_type = Scheduler::clock_type;
using duration = clock_type::duration;
using time_point = clock_type::time_point;
struct LinkType
{
bool inbound = false;
duration delay{};
time_point established;
LinkType() = default;
LinkType(bool inbound, duration delay, time_point established)
: inbound(inbound), delay(delay), established(established)
{
}
};
Scheduler& scheduler_;
Digraph<Peer, LinkType> links_;
public:
BasicNetwork(BasicNetwork const&) = delete;
BasicNetwork&
operator=(BasicNetwork const&) = delete;
BasicNetwork(Scheduler& s);
/**
* Connect two peers.
*
* The link is directed, with `from` establishing
* the outbound connection and `to` receiving the
* incoming connection.
*
* Preconditions:
*
* from != to (self connect disallowed).
*
* A link between from and to does not
* already exist (duplicates disallowed).
*
* Effects:
*
* Creates a link between from and to.
*
* @param `from` The source of the outgoing connection
* @param `to` The recipient of the incoming connection
* @param `delay` The time delay of all delivered messages
* @return `true` if a new connection was established
*/
bool
connect(Peer const& from, Peer const& to, duration const& delay = std::chrono::seconds{0});
/**
* Break a link.
*
* Effects:
*
* If a connection is present, both ends are
* disconnected.
*
* Any pending messages on the connection
* are discarded.
*
* @return `true` if a connection was broken.
*/
bool
disconnect(Peer const& peer1, Peer const& peer2);
/**
* Send a message to a peer.
*
* Preconditions:
*
* A link exists between from and to.
*
* Effects:
*
* If the link is not broken when the
* link's `delay` time has elapsed,
* the function will be invoked with
* no arguments.
*
* @note Its the caller's responsibility to
* ensure that the body of the function performs
* activity consistent with `from`'s receipt of
* a message from `to`.
*/
template <class Function>
void
send(Peer const& from, Peer const& to, Function&& f);
/**
* Return the range of active links.
*
* @return A random access range over Digraph::Edge instances
*/
auto
links(Peer const& from)
{
return links_.outEdges(from);
}
/**
* Return the underlying digraph
*/
[[nodiscard]] Digraph<Peer, LinkType> const&
graph() const
{
return links_;
}
};
//------------------------------------------------------------------------------
template <class Peer>
BasicNetwork<Peer>::BasicNetwork(Scheduler& s) : scheduler_(s)
{
}
template <class Peer>
inline bool
BasicNetwork<Peer>::connect(Peer const& from, Peer const& to, duration const& delay)
{
if (to == from)
return false;
time_point const now = scheduler_.now();
if (!links_.connect(from, to, LinkType{false, delay, now}))
return false;
auto const result = links_.connect(to, from, LinkType{true, delay, now});
(void)result;
assert(result);
return true;
}
template <class Peer>
inline bool
BasicNetwork<Peer>::disconnect(Peer const& peer1, Peer const& peer2)
{
if (!links_.disconnect(peer1, peer2))
return false;
bool const r = links_.disconnect(peer2, peer1);
(void)r;
assert(r);
return true;
}
template <class Peer>
template <class Function>
inline void
BasicNetwork<Peer>::send(Peer const& from, Peer const& to, Function&& f)
{
auto link = links_.edge(from, to);
if (!link)
return;
time_point const sent = scheduler_.now();
scheduler_.in(link->delay, [from, to, sent, f = std::forward<Function>(f), this] {
// only process if still connected and connection was
// not broken since the message was sent
if (auto l = links_.edge(from, to); l && l->established <= sent)
{
f();
}
});
}
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,333 @@
#pragma once
#include <csf/Proposal.h>
#include <csf/SimTime.h>
#include <csf/Tx.h>
#include <csf/Validation.h>
#include <csf/events.h>
#include <csf/ledgers.h>
#include <memory>
#include <vector>
namespace xrpl::test::csf {
/**
* Holds a type-erased reference to an arbitrary collector.
*
* A collector is any class that implements
*
* on(NodeID, SimTime, Event)
*
* for all events emitted by a Peer.
*
* This class is used to type-erase the actual collector used by each peer in
* the simulation. The idea is to compose complicated and typed collectors
* using the helpers in collectors.h, then only type erase at the higher-most
* level when adding to the simulation.
*
* The example code below demonstrates the reason for storing the collector
* as a reference. The collector's lifetime will generally be longer than
* the simulation; perhaps several simulations are run for a single collector
* instance. The collector potentially stores lots of data as well, so the
* simulation needs to point to the single instance, rather than requiring
* collectors to manage copying that data efficiently in their design.
*
* @code
* // Initialize a specific collector that might write to a file.
* SomeFancyCollector collector{"out.file"};
*
* // Setup your simulation
* Sim sim(trustgraph, topology, collector);
*
* // Run the simulation
* sim.run(100);
*
* // do any reported related to the collector
* collector.report();
*
* @endcode
*
* @note If a new event type is added, it needs to be added to the interfaces
* below.
*/
class CollectorRef
{
using tp = SimTime;
// Interface for type-erased collector instance
struct ICollector
{
virtual ~ICollector() = default;
virtual void
on(PeerID node, tp when, Share<Tx> const&) = 0;
virtual void
on(PeerID node, tp when, Share<TxSet> const&) = 0;
virtual void
on(PeerID node, tp when, Share<Validation> const&) = 0;
virtual void
on(PeerID node, tp when, Share<Ledger> const&) = 0;
virtual void
on(PeerID node, tp when, Share<Proposal> const&) = 0;
virtual void
on(PeerID node, tp when, Receive<Tx> const&) = 0;
virtual void
on(PeerID node, tp when, Receive<TxSet> const&) = 0;
virtual void
on(PeerID node, tp when, Receive<Validation> const&) = 0;
virtual void
on(PeerID node, tp when, Receive<Ledger> const&) = 0;
virtual void
on(PeerID node, tp when, Receive<Proposal> const&) = 0;
virtual void
on(PeerID node, tp when, Relay<Tx> const&) = 0;
virtual void
on(PeerID node, tp when, Relay<TxSet> const&) = 0;
virtual void
on(PeerID node, tp when, Relay<Validation> const&) = 0;
virtual void
on(PeerID node, tp when, Relay<Ledger> const&) = 0;
virtual void
on(PeerID node, tp when, Relay<Proposal> const&) = 0;
virtual void
on(PeerID node, tp when, SubmitTx const&) = 0;
virtual void
on(PeerID node, tp when, StartRound const&) = 0;
virtual void
on(PeerID node, tp when, CloseLedger const&) = 0;
virtual void
on(PeerID node, tp when, AcceptLedger const&) = 0;
virtual void
on(PeerID node, tp when, WrongPrevLedger const&) = 0;
virtual void
on(PeerID node, tp when, FullyValidateLedger const&) = 0;
};
// Bridge between type-ful collector T and type erased instance
template <class T>
class Any final : public ICollector
{
T& t_;
public:
Any(T& t) : t_{t}
{
}
// Can't copy
Any(Any const&) = delete;
Any&
operator=(Any const&) = delete;
Any(Any&&) = default;
Any&
operator=(Any&&) = default;
void
on(PeerID node, tp when, Share<Tx> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Share<TxSet> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Share<Validation> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Share<Ledger> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Share<Proposal> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Receive<Tx> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Receive<TxSet> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Receive<Validation> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Receive<Ledger> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Receive<Proposal> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Relay<Tx> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Relay<TxSet> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Relay<Validation> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Relay<Ledger> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, Relay<Proposal> const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, SubmitTx const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, StartRound const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, CloseLedger const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, AcceptLedger const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, WrongPrevLedger const& e) override
{
t_.on(node, when, e);
}
void
on(PeerID node, tp when, FullyValidateLedger const& e) override
{
t_.on(node, when, e);
}
};
std::unique_ptr<ICollector> impl_;
public:
template <class T>
CollectorRef(T& t) : impl_{new Any<T>(t)}
{
}
// Non-copyable
CollectorRef(CollectorRef const& c) = delete;
CollectorRef&
operator=(CollectorRef& c) = delete;
CollectorRef(CollectorRef&&) = default;
CollectorRef&
operator=(CollectorRef&&) = default;
template <class E>
void
on(PeerID node, tp when, E const& e)
{
impl_->on(node, when, e);
}
};
/**
* A container of CollectorRefs
*
* A set of CollectorRef instances that process the same events. An event is
* processed by collectors in the order the collectors were added.
*
* This class type-erases the collector instances. By contract, the
* Collectors/collectors class/helper in collectors.h are not type erased and
* offer an opportunity for type transformations and combinations with
* improved compiler optimizations.
*/
class CollectorRefs
{
std::vector<CollectorRef> collectors_;
public:
template <class Collector>
void
add(Collector& collector)
{
collectors_.emplace_back(collector);
}
template <class E>
void
on(PeerID node, SimTime when, E const& e)
{
for (auto& c : collectors_)
{
c.on(node, when, e);
}
}
};
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,72 @@
#include <csf/Digraph.h>
#include <gtest/gtest.h>
#include <sstream>
#include <string>
#include <tuple>
#include <vector>
namespace xrpl::test {
TEST(DigraphTest, digraph)
{
using namespace csf;
using Graph = Digraph<char, std::string>;
Graph graph;
EXPECT_TRUE(!graph.connected('a', 'b'));
EXPECT_TRUE(!graph.edge('a', 'b'));
EXPECT_TRUE(!graph.disconnect('a', 'b'));
EXPECT_TRUE(graph.connect('a', 'b', "foobar"));
EXPECT_TRUE(graph.connected('a', 'b'));
EXPECT_TRUE(*graph.edge('a', 'b') == "foobar"); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_TRUE(!graph.connect('a', 'b', "repeat"));
EXPECT_TRUE(graph.disconnect('a', 'b'));
EXPECT_TRUE(graph.connect('a', 'b', "repeat"));
EXPECT_TRUE(graph.connected('a', 'b'));
EXPECT_TRUE(*graph.edge('a', 'b') == "repeat"); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_TRUE(graph.connect('a', 'c', "tree"));
{
std::vector<std::tuple<char, char, std::string>> edges;
for (auto const& edge : graph.outEdges('a'))
{
edges.emplace_back(edge.source, edge.target, edge.data);
}
std::vector<std::tuple<char, char, std::string>> expected;
expected.emplace_back('a', 'b', "repeat");
expected.emplace_back('a', 'c', "tree");
EXPECT_TRUE(edges == expected);
EXPECT_TRUE(graph.outDegree('a') == expected.size());
}
EXPECT_TRUE(graph.outEdges('r').size() == 0);
EXPECT_TRUE(graph.outDegree('r') == 0);
EXPECT_TRUE(graph.outDegree('c') == 0);
// only 'a' has out edges
EXPECT_TRUE(graph.outVertices().size() == 1);
std::vector<char> const expected = {'b', 'c'};
EXPECT_TRUE((graph.outVertices('a') == expected));
EXPECT_TRUE(graph.outVertices('b').size() == 0);
EXPECT_TRUE(graph.outVertices('c').size() == 0);
EXPECT_TRUE(graph.outVertices('r').size() == 0);
std::stringstream ss;
graph.saveDot(ss, [](char v) { return v; });
std::string const expectedDot =
"digraph {\n"
"a -> b;\n"
"a -> c;\n"
"}\n";
EXPECT_TRUE(ss.str() == expectedDot);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,238 @@
#pragma once
#include <boost/container/flat_map.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/iterator_range.hpp>
#include <cstddef>
#include <fstream>
#include <optional>
#include <ostream>
#include <string>
namespace xrpl {
namespace detail {
// Dummy class when no edge data needed for graph
struct NoEdgeData
{
};
} // namespace detail
namespace test::csf {
/**
* Directed graph
*
* Basic directed graph that uses an adjacency list to represent out edges.
*
* Instances of Vertex uniquely identify vertices in the graph. Instances of
* EdgeData is any data to store in the edge connecting two vertices.
*
* Both Vertex and EdgeData should be lightweight and cheap to copy.
*/
template <class Vertex, class EdgeData = detail::NoEdgeData>
class Digraph
{
using Links = boost::container::flat_map<Vertex, EdgeData>;
using Graph = boost::container::flat_map<Vertex, Links>;
Graph graph_;
// Allows returning empty iterables for unknown vertices
Links empty_;
public:
/**
* Connect two vertices
*
* @param source The source vertex
* @param target The target vertex
* @param e The edge data
* @return true if the edge was created
*/
bool
connect(Vertex source, Vertex target, EdgeData e)
{
return graph_[source].emplace(target, e).second;
}
/**
* Connect two vertices using default constructed edge data
*
* @param source The source vertex
* @param target The target vertex
* @return true if the edge was created
*/
bool
connect(Vertex source, Vertex target)
{
return connect(source, target, EdgeData{});
}
/**
* Disconnect two vertices
*
* @param source The source vertex
* @param target The target vertex
* @return true if an edge was removed
*
* If source is not connected to target, this function does nothing.
*/
bool
disconnect(Vertex source, Vertex target)
{
auto it = graph_.find(source);
if (it != graph_.end())
{
return it->second.erase(target) > 0;
}
return false;
}
/**
* Return edge data between two vertices
*
* @param source The source vertex
* @param target The target vertex
* @return optional<Edge> which is std::nullopt if no edge exists
*/
[[nodiscard]] std::optional<EdgeData>
edge(Vertex source, Vertex target) const
{
auto it = graph_.find(source);
if (it != graph_.end())
{
auto edgeIt = it->second.find(target);
if (edgeIt != it->second.end())
return edgeIt->second;
}
return std::nullopt;
}
/**
* Check if two vertices are connected
*
* @param source The source vertex
* @param target The target vertex
* @return true if the source has an out edge to target
*/
[[nodiscard]] bool
connected(Vertex source, Vertex target) const
{
return edge(source, target) != std::nullopt;
}
/**
* Range over vertices in the graph
*
* @return A boost transformed range over the vertices with out edges in
* the graph
*/
[[nodiscard]] auto
outVertices() const
{
return boost::adaptors::transform(
graph_, [](Graph::value_type const& v) { return v.first; });
}
/**
* Range over target vertices
*
* @param source The source vertex
* @return A boost transformed range over the target vertices of source.
*/
[[nodiscard]] auto
outVertices(Vertex source) const
{
auto transform = [](Links::value_type const& link) { return link.first; };
auto it = graph_.find(source);
if (it != graph_.end())
return boost::adaptors::transform(it->second, transform);
return boost::adaptors::transform(empty_, transform);
}
/**
* Vertices and data associated with an Edge
*/
struct Edge
{
Vertex source;
Vertex target;
EdgeData data;
};
/**
* Range of out edges
*
* @param source The source vertex
* @return A boost transformed range of Edge type for all out edges of
* source.
*/
[[nodiscard]] auto
outEdges(Vertex source) const
{
auto transform = [source](Links::value_type const& link) {
return Edge{source, link.first, link.second};
};
auto it = graph_.find(source);
if (it != graph_.end())
return boost::adaptors::transform(it->second, transform);
return boost::adaptors::transform(empty_, transform);
}
/**
* Vertex out-degree
*
* @param source The source vertex
* @return The number of outgoing edges from source
*/
[[nodiscard]] std::size_t
outDegree(Vertex source) const
{
auto it = graph_.find(source);
if (it != graph_.end())
return it->second.size();
return 0;
}
/**
* Save GraphViz dot file
*
* Save a GraphViz dot description of the graph
* @param fileName The output file (creates)
* @param vertexName A invocable T vertexName(Vertex const &) that
* returns the name target use for the vertex in the file
* T must be ostream-able
*/
template <class VertexName>
void
saveDot(std::ostream& out, VertexName&& vertexName) const
{
out << "digraph {\n";
for (auto const& [vertex, links] : graph_)
{
auto const fromName = vertexName(vertex);
for (auto const& eData : links)
{
auto const toName = vertexName(eData.first);
out << fromName << " -> " << toName << ";\n";
}
}
out << "}\n";
}
template <class VertexName>
void
saveDot(std::string const& fileName, VertexName&& vertexName) const
{
std::ofstream out(fileName);
saveDot(out, std::forward<VertexName>(vertexName));
}
};
} // namespace test::csf
} // namespace xrpl

View File

@@ -0,0 +1,59 @@
#include <csf/Histogram.h>
#include <gtest/gtest.h>
namespace xrpl::test {
TEST(HistogramTest, histogram)
{
using namespace csf;
Histogram<int> hist;
EXPECT_TRUE(hist.size() == 0);
EXPECT_TRUE(hist.numBins() == 0);
EXPECT_TRUE(hist.minValue() == 0);
EXPECT_TRUE(hist.maxValue() == 0);
EXPECT_TRUE(hist.avg() == 0);
EXPECT_TRUE(hist.percentile(0.0f) == hist.minValue());
EXPECT_TRUE(hist.percentile(0.5f) == 0);
EXPECT_TRUE(hist.percentile(0.9f) == 0);
EXPECT_TRUE(hist.percentile(1.0f) == hist.maxValue());
hist.insert(1);
EXPECT_TRUE(hist.size() == 1);
EXPECT_TRUE(hist.numBins() == 1);
EXPECT_TRUE(hist.minValue() == 1);
EXPECT_TRUE(hist.maxValue() == 1);
EXPECT_TRUE(hist.avg() == 1);
EXPECT_TRUE(hist.percentile(0.0f) == hist.minValue());
EXPECT_TRUE(hist.percentile(0.5f) == 1);
EXPECT_TRUE(hist.percentile(0.9f) == 1);
EXPECT_TRUE(hist.percentile(1.0f) == hist.maxValue());
hist.insert(9);
EXPECT_TRUE(hist.size() == 2);
EXPECT_TRUE(hist.numBins() == 2);
EXPECT_TRUE(hist.minValue() == 1);
EXPECT_TRUE(hist.maxValue() == 9);
EXPECT_TRUE(hist.avg() == 5);
EXPECT_TRUE(hist.percentile(0.0f) == hist.minValue());
EXPECT_TRUE(hist.percentile(0.5f) == 1);
EXPECT_TRUE(hist.percentile(0.9f) == 9);
EXPECT_TRUE(hist.percentile(1.0f) == hist.maxValue());
hist.insert(1);
EXPECT_TRUE(hist.size() == 3);
EXPECT_TRUE(hist.numBins() == 2);
EXPECT_TRUE(hist.minValue() == 1);
EXPECT_TRUE(hist.maxValue() == 9);
EXPECT_TRUE(hist.avg() == 11 / 3);
EXPECT_TRUE(hist.percentile(0.0f) == hist.minValue());
EXPECT_TRUE(hist.percentile(0.5f) == 1);
EXPECT_TRUE(hist.percentile(0.9f) == 9);
EXPECT_TRUE(hist.percentile(1.0f) == hist.maxValue());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,121 @@
#pragma once
#include <cassert>
#include <cmath>
#include <cstddef>
#include <functional>
#include <map>
namespace xrpl::test::csf {
/**
* Basic histogram.
*
* Histogram for a type `T` that satisfies
* - Default construction: T{}
* - Comparison : T a, b; bool res = a < b
* - Addition: T a, b; T c = a + b;
* - Multiplication : T a, std::size_t b; T c = a * b;
* - Division: T a; std::size_t b; T c = a/b;
*/
template <class T, class Compare = std::less<T>>
class Histogram
{
// TODO: Consider logarithmic bins around expected median if this becomes
// unscalable
std::map<T, std::size_t, Compare> counts_;
std::size_t samples_ = 0;
public:
/**
* Insert an sample
*/
void
insert(T const& s)
{
++counts_[s];
++samples_;
}
/**
* The number of samples
*/
[[nodiscard]] std::size_t
size() const
{
return samples_;
}
/**
* The number of distinct samples (bins)
*/
[[nodiscard]] std::size_t
numBins() const
{
return counts_.size();
}
/**
* Minimum observed value
*/
[[nodiscard]] T
minValue() const
{
return counts_.empty() ? T{} : counts_.begin()->first;
}
/**
* Maximum observed value
*/
[[nodiscard]] T
maxValue() const
{
return counts_.empty() ? T{} : counts_.rbegin()->first;
}
/**
* Histogram average
*/
[[nodiscard]] T
avg() const
{
T tmp{};
if (samples_ == 0)
return tmp;
// Since counts are sorted, shouldn't need to worry much about numerical
// error
for (auto const& [bin, count] : counts_)
{
tmp += bin * count;
}
return tmp / samples_;
}
/**
* Calculate the given percentile of the distribution.
*
* @param p Percentile between 0 and 1, e.g. 0.50 is 50-th percentile
* If the percentile falls between two bins, uses the nearest bin.
* @return The given percentile of the distribution
*/
[[nodiscard]] T
percentile(float p) const
{
assert(p >= 0 && p <= 1);
std::size_t const pos = std::round(p * samples_);
if (counts_.empty())
return T{};
auto it = counts_.begin();
std::size_t cumsum = it->second;
while (it != counts_.end() && cumsum < pos)
{
++it;
cumsum += it->second;
}
return it->first;
}
};
} // namespace xrpl::test::csf

1070
src/tests/libxrpl/csf/Peer.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,351 @@
#pragma once
#include <csf/Peer.h>
#include <csf/SimTime.h>
#include <csf/Validation.h>
#include <csf/random.h>
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <iterator>
#include <ostream>
#include <random>
#include <set>
#include <utility>
#include <vector>
namespace xrpl::test::csf {
/**
* A group of simulation Peers
*
* A PeerGroup is a convenient handle for logically grouping peers together,
* and then creating trust or network relations for the group at large. Peer
* groups may also be combined to build out more complex structures.
*
* The PeerGroup provides random access style iterators and operator[]
*/
class PeerGroup
{
using peers_type = std::vector<Peer*>;
peers_type peers_;
public:
using iterator = peers_type::iterator;
using const_iterator = peers_type::const_iterator;
using reference = peers_type::reference;
using const_reference = peers_type::const_reference;
PeerGroup() = default;
PeerGroup(Peer* peer) : peers_{1, peer}
{
}
PeerGroup(std::vector<Peer*>&& peers) : peers_{std::move(peers)}
{
std::ranges::sort(peers_);
}
PeerGroup(std::vector<Peer*> const& peers) : peers_{peers}
{
std::ranges::sort(peers_);
}
PeerGroup(std::set<Peer*> const& peers) : peers_{peers.begin(), peers.end()}
{
}
iterator
begin()
{
return peers_.begin();
}
iterator
end()
{
return peers_.end();
}
[[nodiscard]] const_iterator
begin() const
{
return peers_.begin();
}
[[nodiscard]] const_iterator
end() const
{
return peers_.end();
}
const_reference
operator[](std::size_t i) const
{
return peers_[i];
}
bool
contains(Peer const* p)
{
return std::ranges::find(peers_, p) != peers_.end();
}
bool
contains(PeerID id)
{
return std::ranges::find_if(peers_, [id](Peer const* p) { return p->id == id; }) !=
peers_.end();
}
[[nodiscard]] std::size_t
size() const
{
return peers_.size();
}
/**
* Establish trust
*
* Establish trust from all peers in this group to all peers in o
*
* @param o The group of peers to trust
*/
void
trust(PeerGroup const& o)
{
for (Peer* p : peers_)
{
for (Peer* target : o.peers_)
{
p->trust(*target);
}
}
}
/**
* Revoke trust
*
* Revoke trust from all peers in this group to all peers in o
*
* @param o The group of peers to untrust
*/
void
untrust(PeerGroup const& o)
{
for (Peer* p : peers_)
{
for (Peer* target : o.peers_)
{
p->untrust(*target);
}
}
}
/**
* Establish network connection
*
* Establish outbound connections from all peers in this group to all peers
* in o. If a connection already exists, no new connection is established.
*
* @param o The group of peers to connect to (will get inbound connections)
* @param delay The fixed messaging delay for all established connections
*/
void
connect(PeerGroup const& o, SimDuration delay)
{
for (Peer* p : peers_)
{
for (Peer* target : o.peers_)
{
// cannot send messages to self over network
if (p != target)
p->connect(*target, delay);
}
}
}
/**
* Destroy network connection
*
* Destroy connections from all peers in this group to all peers in o
*
* @param o The group of peers to disconnect from
*/
void
disconnect(PeerGroup const& o)
{
for (Peer* p : peers_)
{
for (Peer* target : o.peers_)
{
p->disconnect(*target);
}
}
}
/**
* Establish trust and network connection
*
* Establish trust and create a network connection with fixed delay
* from all peers in this group to all peers in o
*
* @param o The group of peers to trust and connect to
* @param delay The fixed messaging delay for all established connections
*/
void
trustAndConnect(PeerGroup const& o, SimDuration delay)
{
trust(o);
connect(o, delay);
}
/**
* Establish network connections based on trust relations
*
* For each peers in this group, create outbound network connection
* to the set of peers it trusts. If a connection already exists, it is
* not recreated.
*
* @param delay The fixed messaging delay for all established connections
*/
void
connectFromTrust(SimDuration delay)
{
for (Peer* peer : peers_)
{
for (Peer* to : peer->trustGraph.trustedPeers(peer))
{
peer->connect(*to, delay);
}
}
}
// Union of PeerGroups
friend PeerGroup
operator+(PeerGroup const& a, PeerGroup const& b)
{
PeerGroup res;
std::ranges::set_union(a.peers_, b.peers_, std::back_inserter(res.peers_));
return res;
}
// Set difference of PeerGroups
friend PeerGroup
operator-(PeerGroup const& a, PeerGroup const& b)
{
PeerGroup res;
std::ranges::set_difference(a.peers_, b.peers_, std::back_inserter(res.peers_));
return res;
}
friend std::ostream&
operator<<(std::ostream& o, PeerGroup const& t)
{
o << "{";
bool first = true;
for (Peer const* p : t)
{
if (!first)
o << ", ";
first = false;
o << p->id;
}
o << "}";
return o;
}
};
/**
* Randomly generate peer groups according to ranks.
*
* Generates random peer groups based on a provided ranking of peers. This
* mimics a process of randomly generating UNLs, where more "important" peers
* are more likely to appear in a UNL.
*
* `numGroups` subgroups are generated by randomly sampling without without
* replacement from peers according to the `ranks`.
*
*
*
* @param peers The group of peers
* @param ranks The relative importance of each peer, must match the size of
* peers. Higher relative rank means more likely to be sampled.
* @param numGroups The number of peer link groups to generate
* @param sizeDist The distribution that determines the size of a link group
* @param g The uniform random bit generator
*/
template <class RandomNumberDistribution, class Generator>
std::vector<PeerGroup>
randomRankedGroups(
PeerGroup& peers,
std::vector<double> const& ranks,
int numGroups,
RandomNumberDistribution sizeDist,
Generator& g)
{
assert(peers.size() == ranks.size());
std::vector<PeerGroup> groups;
groups.reserve(numGroups);
std::vector<Peer*> rawPeers(peers.begin(), peers.end());
std::generate_n(std::back_inserter(groups), numGroups, [&]() {
std::vector<Peer*> res = randomWeightedShuffle(rawPeers, ranks, g);
res.resize(sizeDist(g));
return PeerGroup(std::move(res));
});
return groups;
}
/**
* Generate random trust groups based on peer rankings.
*
* @see randomRankedGroups for descriptions of the arguments
*/
template <class RandomNumberDistribution, class Generator>
void
randomRankedTrust(
PeerGroup& peers,
std::vector<double> const& ranks,
int numGroups,
RandomNumberDistribution sizeDist,
Generator& g)
{
std::vector<PeerGroup> const groups = randomRankedGroups(peers, ranks, numGroups, sizeDist, g);
std::uniform_int_distribution<int> u(0, groups.size() - 1); // NOLINT(misc-const-correctness)
for (auto& peer : peers)
{
for (auto& target : groups[u(g)])
peer->trust(*target);
}
}
/**
* Generate random network groups based on peer rankings.
*
* @see randomRankedGroups for descriptions of the arguments
*/
template <class RandomNumberDistribution, class Generator>
void
randomRankedConnect(
PeerGroup& peers,
std::vector<double> const& ranks,
int numGroups,
RandomNumberDistribution sizeDist,
Generator& g,
SimDuration delay)
{
std::vector<PeerGroup> const groups = randomRankedGroups(peers, ranks, numGroups, sizeDist, g);
std::uniform_int_distribution<int> u(0, groups.size() - 1); // NOLINT(misc-const-correctness)
for (auto& peer : peers)
{
for (auto& target : groups[u(g)])
peer->connect(*target, delay);
}
}
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,16 @@
#pragma once
#include <xrpl/consensus/ConsensusProposal.h>
#include <csf/Tx.h>
#include <csf/Validation.h>
#include <csf/ledgers.h>
namespace xrpl::test::csf {
/**
* Proposal is a position taken in the consensus process and is represented
* directly from the generic types.
*/
using Proposal = ConsensusProposal<PeerID, Ledger::ID, TxSet::ID>;
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,192 @@
# Consensus Simulation Framework
The Consensus Simulation Framework is a set of software components for
describing, running and analyzing simulations of the consensus algorithm in a
controlled manner. It is also used to unit test the generic XRPL consensus
algorithm implementation. The framework is in its early stages, so the design
and supported features are subject to change.
## Overview
The simulation framework focuses on simulating the core consensus and validation
algorithms as a [discrete event
simulation](https://en.wikipedia.org/wiki/Discrete_event_simulation). It is
completely abstracted from the details of the XRP ledger and transactions. In
the simulation, a ledger is simply a set of observed integers and transactions
are single integers. The consensus process works to agree on the set of integers
to include in the next ledger.
![CSF Overview](./csf_overview.png "CSF Overview")
The diagram above gives a stylized overview of the components provided by the
framework. These are combined by the simulation author into the simulation
specification, which defines the configuration of the system and the data to
collect when running the simulation. The specification includes:
- A collection of [`Peer`s](./Peer.h) that represent the participants in the
network, with each independently running the consensus algorithm.
- The `Peer` trust relationships as a `TrustGraph`. This is a directed graph
whose edges define what other `Peer`s a given `Peer` trusts. In other words,
the set of out edges for a `Peer` in the graph correspond to the UNL of that
`Peer`.
- The network communication layer as a `BasicNetwork`. This models the overlay
network topology in which messages are routed between `Peer`s. This graph
topology can be configured independently from the `TrustGraph`.
- Transaction `Submitter`s that model the submission of client transactions to
the network.
- `Collector`s that aggregate, filter and analyze data from the simulation.
Typically, this is used to monitor invariants or generate reports.
Once specified, the simulation runs using a single `Scheduler` that manages the
global clock and sequencing of activity. During the course of simulation,
`Peer`s generate `Ledger`s and `Validation`s as a result of consensus,
eventually fully validating the consensus history of accepted transactions. Each
`Peer` also issues various `Event`s during the simulation, which are analyzed by
the registered `Collector`s.
## Example Simulation
Below is a basic simulation we can walk through to get an understanding of the
framework. This simulation is for a set of 5 validators that aren't directly
connected but rely on a single hub node for communication.
![Example Sim](./csf_graph.png "Example Sim")
Each Peer has a unique transaction submitted, then runs one round of the
consensus algorithm.
```c++
Sim sim;
PeerGroup validators = sim.createGroup(5);
PeerGroup center = sim.createGroup(1);
PeerGroup network = validators + center;
center[0]->runAsValidator = false;
validators.trust(validators);
center.trust(validators);
using namespace std::chrono;
SimDuration delay = 200ms;
validators.connect(center, delay);
SimDurationCollector simDur;
sim.collectors.add(simDur);
// prep round to set initial state.
sim.run(1);
// everyone submits their own ID as a TX and relay it to peers
for (Peer * p : validators)
p->submit(Tx(static_cast<std::uint32_t>(p->id)));
sim.run(1);
std::cout << (simDur.stop - simDur.start).count() << std::endl;
assert(sim.synchronized());
```
### `Sim` and `PeerGroup`
```c++
Sim sim;
PeerGroup validators = sim.createGroup(5);
PeerGroup center = sim.createGroup(1);
PeerGroup network = validators + center;
center[0]->runAsValidator = false;
```
The simulation code starts by creating a single instance of the [`Sim`
class](./Sim.h). This class is used to manage the overall simulation and
internally owns most other components, including the `Peer`s, `Scheduler`,
`BasicNetwork` and `TrustGraph`. The next two lines create two differ
`PeerGroup`s of size 5 and 1 . A [`PeerGroup`](./PeerGroup.h) is a convenient
way for configuring a set of related peers together and internally has a vector
of pointers to the `Peer`s which are owned by the `Sim`. `PeerGroup`s can be
combined using `+/-` operators to configure more complex relationships of nodes
as shown by `PeerGroup network`. Note that each call to `createGroup` adds that
many new `Peer`s to the simulation, but does not specify any trust or network
relationships for the new `Peer`s.
Lastly, the single `Peer` in the size 1 `center` group is switched from running
as a validator (the default) to running as a tracking peer. The [`Peer`
class](./Peer.h) has a variety of configurable parameters that control how it
behaves during the simulation.
## `trust` and `connect`
```c++
validators.trust(validators);
center.trust(validators);
using namespace std::chrono;
SimDuration delay = 200ms;
validators.connect(center, delay);
```
Although the `sim` object has accessible instances of
[TrustGraph](./TrustGraph.h) and [BasicNetwork](./BasicNetwork.h), it is more
convenient to manage the graphs via the `PeerGroup`s. The first two lines
create a trust topology in which all `Peer`s trust the 5 validating `Peer`s. Or
in the UNL perspective, all `Peer`s are configured with the same UNL listing the
5 validating `Peer`s. The two lines could've been rewritten as
`network.trust(validators)`.
The next lines create the network communication topology. Each of the validating
`Peer`s connects to the central hub `Peer` with a fixed delay of 200ms. Note
that the network connections are really undirected, but are represented
internally in a directed graph using edge pairs of inbound and outbound connections.
## Collectors
```c++
SimDurationCollector simDur;
sim.collectors.add(simDur);
```
The next lines add a single collector to the simulation. The
`SimDurationCollector` is a simple example collector which tracks the total
duration of the simulation. More generally, a collector is any class that
implements `void on(NodeID, SimTime, Event)` for all [Events](./events.h)
emitted by a Peer. Events are arbitrary types used to indicate some action or
change of state of a `Peer`. Other [existing collectors](./collectors.h) measure
latencies of transaction submission to validation or the rate of ledger closing
and monitor any jumps in ledger history.
Note that the collector lifetime is independent of the simulation and is added
to the simulation by reference. This is intentional, since collectors might be
used across several simulations to collect more complex combinations of data. At
the end of the simulation, we print out the total duration by subtracting
`simDur` members.
```c++
std::cout << (simDur.stop - simDur.start).count() << std::endl;
```
## Transaction submission
```c++
// everyone submits their own ID as a TX and relay it to peers
for (Peer * p : validators)
p->submit(Tx(static_cast<std::uint32_t>(p->id)));
```
In this basic example, we explicitly submit a single transaction to each
validator. For larger simulations, clients can use a [Submitter](./submitters.h)
to send transactions in at fixed or random intervals to fixed or random `Peer`s.
## Run
The example has two calls to `sim.run(1)`. This call runs the simulation until
each `Peer` has closed one additional ledger. After closing the additional
ledger, the `Peer` stops participating in consensus. The first call is used to
ensure a more useful prior state of all `Peer`s. After the transaction
submission, the second call to `run` results in one additional ledger that
accepts those transactions.
Alternatively, you can specify a duration to run the simulation, e.g.
`sim.run(10s)` which would have `Peer`s continuously run consensus until the
scheduler has elapsed 10 additional seconds. The `sim.scheduler.in` or
`sim.scheduler.at` methods can schedule arbitrary code to execute at a later
time in the simulation, for example removing a network connection or modifying
the trust graph.

View File

@@ -0,0 +1,61 @@
#include <csf/Scheduler.h>
#include <gtest/gtest.h>
#include <set>
namespace xrpl::test {
TEST(SchedulerTest, scheduler)
{
using namespace std::chrono_literals;
csf::Scheduler scheduler;
std::set<int> seen;
scheduler.in(1s, [&] { seen.insert(1); });
scheduler.in(2s, [&] { seen.insert(2); });
auto token = scheduler.in(3s, [&] { seen.insert(3); });
scheduler.at(scheduler.now() + 4s, [&] { seen.insert(4); });
scheduler.at(scheduler.now() + 8s, [&] { seen.insert(8); });
auto start = scheduler.now();
// Process first event
EXPECT_TRUE(seen.empty());
EXPECT_TRUE(scheduler.stepOne());
EXPECT_TRUE(seen == std::set<int>({1}));
EXPECT_TRUE(scheduler.now() == (start + 1s));
// No processing if stepping until current time
EXPECT_TRUE(scheduler.stepUntil(scheduler.now()));
EXPECT_TRUE(seen == std::set<int>({1}));
EXPECT_TRUE(scheduler.now() == (start + 1s));
// Process next event
EXPECT_TRUE(scheduler.stepFor(1s));
EXPECT_TRUE(seen == std::set<int>({1, 2}));
EXPECT_TRUE(scheduler.now() == (start + 2s));
// Don't process cancelled event, but advance clock
scheduler.cancel(token);
EXPECT_TRUE(scheduler.stepFor(1s));
EXPECT_TRUE(seen == std::set<int>({1, 2}));
EXPECT_TRUE(scheduler.now() == (start + 3s));
// Process until 3 seen ints
EXPECT_TRUE(scheduler.stepWhile([&]() { return seen.size() < 3; }));
EXPECT_TRUE(seen == std::set<int>({1, 2, 4}));
EXPECT_TRUE(scheduler.now() == (start + 4s));
// Process the rest
EXPECT_TRUE(scheduler.step());
EXPECT_TRUE(seen == std::set<int>({1, 2, 4, 8}));
EXPECT_TRUE(scheduler.now() == (start + 8s));
// Process the rest again doesn't advance
EXPECT_TRUE(!scheduler.step());
EXPECT_TRUE(seen == std::set<int>({1, 2, 4, 8}));
EXPECT_TRUE(scheduler.now() == (start + 8s));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,443 @@
#pragma once
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/beast/clock/manual_clock.h>
#include <boost/container/pmr/monotonic_buffer_resource.hpp>
#include <boost/intrusive/set.hpp>
#include <chrono>
#include <type_traits>
#include <utility>
namespace xrpl::test::csf {
/**
* Simulated discrete-event scheduler.
*
* Simulates the behavior of events using a single common clock.
*
* An event is modeled using a lambda function and is scheduled to occur at a
* specific time. Events may be canceled using a token returned when the
* event is scheduled.
*
* The caller uses one or more of the step, stepOne, stepFor, stepUntil and
* stepWhile functions to process scheduled events.
*/
class Scheduler
{
public:
using clock_type = beast::ManualClock<std::chrono::steady_clock>;
using duration = clock_type::duration;
using time_point = clock_type::time_point;
private:
using by_when_hook =
boost::intrusive::set_base_hook<boost::intrusive::link_mode<boost::intrusive::normal_link>>;
struct Event : by_when_hook
{
time_point when;
Event(Event const&) = delete;
Event&
operator=(Event const&) = delete;
virtual ~Event() = default;
// Called to perform the event
virtual void
operator()() const = 0;
Event(time_point when) : when(when)
{
}
bool
operator<(Event const& other) const
{
return when < other.when;
}
};
template <class Handler>
class EventImpl : public Event
{
Handler const h_;
public:
EventImpl(EventImpl const&) = delete;
EventImpl&
operator=(EventImpl const&) = delete;
template <class DeducedHandler>
EventImpl(time_point when, DeducedHandler&& h)
: Event(when), h_(std::forward<DeducedHandler>(h))
{
}
void
operator()() const override
{
h_();
}
};
class QueueType
{
private:
using by_when_set = boost::intrusive::
make_multiset<Event, boost::intrusive::constant_time_size<false>>::type;
// alloc_ is owned by the scheduler
boost::container::pmr::monotonic_buffer_resource* alloc_;
by_when_set byWhen_;
public:
using iterator = by_when_set::iterator;
QueueType(QueueType const&) = delete;
QueueType&
operator=(QueueType const&) = delete;
explicit QueueType(boost::container::pmr::monotonic_buffer_resource* alloc);
~QueueType();
[[nodiscard]] bool
empty() const;
iterator
begin();
iterator
end();
template <class Handler>
by_when_set::iterator
emplace(time_point when, Handler&& h);
iterator
erase(iterator iter);
};
boost::container::pmr::monotonic_buffer_resource alloc_{kilobytes(256)};
QueueType queue_;
// Aged containers that rely on this clock take a non-const reference =(
mutable clock_type clock_;
public:
Scheduler(Scheduler const&) = delete;
Scheduler&
operator=(Scheduler const&) = delete;
Scheduler();
/**
* Return the clock. (aged_containers want a non-const ref =(
*/
clock_type&
clock() const;
/**
* Return the current network time.
*
* @note The epoch is unspecified
*/
time_point
now() const;
// Used to cancel timers
struct CancelToken;
/**
* Schedule an event at a specific time
*
* Effects:
*
* When the network time is reached,
* the function will be called with
* no arguments.
*/
template <class Function>
CancelToken
at(time_point const& when, Function&& f);
/**
* Schedule an event after a specified duration passes
*
* Effects:
*
* When the specified time has elapsed,
* the function will be called with
* no arguments.
*/
template <class Function>
CancelToken
in(duration const& delay, Function&& f);
/**
* Cancel a timer.
*
* Preconditions:
*
* `token` was the return value of a call
* timer() which has not yet been invoked.
*/
void
cancel(CancelToken const& token);
/**
* Run the scheduler for up to one event.
*
* Effects:
*
* The clock is advanced to the time
* of the last delivered event.
*
* @return `true` if an event was processed.
*/
bool
stepOne();
/**
* Run the scheduler until no events remain.
*
* Effects:
*
* The clock is advanced to the time
* of the last event.
*
* @return `true` if an event was processed.
*/
bool
step();
/**
* Run the scheduler while a condition is true.
*
* Function takes no arguments and will be called
* repeatedly after each event is processed to
* decide whether to continue.
*
* Effects:
*
* The clock is advanced to the time
* of the last delivered event.
*
* @return `true` if any event was processed.
*/
template <class Function>
bool
stepWhile(Function&& func);
/**
* Run the scheduler until the specified time.
*
* Effects:
*
* The clock is advanced to the
* specified time.
*
* @return `true` if any event remain.
*/
bool
stepUntil(time_point const& until);
/**
* Run the scheduler until time has elapsed.
*
* Effects:
*
* The clock is advanced by the
* specified duration.
*
* @return `true` if any event remain.
*/
template <class Period, class Rep>
bool
stepFor(std::chrono::duration<Period, Rep> const& amount);
};
//------------------------------------------------------------------------------
inline Scheduler::QueueType::QueueType(boost::container::pmr::monotonic_buffer_resource* alloc)
: alloc_(alloc)
{
}
inline Scheduler::QueueType::~QueueType()
{
for (auto iter = byWhen_.begin(); iter != byWhen_.end();)
{
auto e = &*iter;
++iter;
e->~Event();
alloc_->deallocate(e, sizeof(e)); // NOLINT(bugprone-sizeof-expression)
}
}
inline bool
Scheduler::QueueType::empty() const
{
return byWhen_.empty();
}
inline auto
Scheduler::QueueType::begin() -> iterator
{
return byWhen_.begin();
}
inline auto
Scheduler::QueueType::end() -> iterator
{
return byWhen_.end();
}
template <class Handler>
inline auto
Scheduler::QueueType::emplace(time_point when, Handler&& h) -> by_when_set::iterator
{
using event_type = EventImpl<std::decay_t<Handler>>;
auto const p = alloc_->allocate(sizeof(event_type));
auto& e = *new (p) event_type(when, std::forward<Handler>(h));
return byWhen_.insert(e);
}
inline auto
Scheduler::QueueType::erase(iterator iter) -> by_when_set::iterator
{
auto& e = *iter;
auto next = byWhen_.erase(iter);
e.~Event();
alloc_->deallocate(&e, sizeof(e));
return next;
}
//-----------------------------------------------------------------------------
struct Scheduler::CancelToken
{
private:
QueueType::iterator iter_;
public:
CancelToken() = delete;
CancelToken(CancelToken const&) = default;
CancelToken&
operator=(CancelToken const&) = default;
private:
friend class Scheduler;
CancelToken(QueueType::iterator iter) : iter_(iter)
{
}
};
//------------------------------------------------------------------------------
inline Scheduler::Scheduler() : queue_(&alloc_)
{
}
inline auto
Scheduler::clock() const -> clock_type&
{
return clock_;
}
inline auto
Scheduler::now() const -> time_point
{
return clock_.now();
}
template <class Function>
inline auto
Scheduler::at(time_point const& when, Function&& f) -> CancelToken
{
return queue_.emplace(when, std::forward<Function>(f));
}
template <class Function>
inline auto
Scheduler::in(duration const& delay, Function&& f) -> CancelToken
{
return at(clock_.now() + delay, std::forward<Function>(f));
}
inline void
Scheduler::cancel(CancelToken const& token)
{
queue_.erase(token.iter_);
}
inline bool
Scheduler::stepOne()
{
if (queue_.empty())
return false;
auto const iter = queue_.begin();
clock_.set(iter->when);
(*iter)();
queue_.erase(iter);
return true;
}
inline bool
Scheduler::step()
{
if (!stepOne())
return false;
for (;;)
{
if (!stepOne())
break;
}
return true;
}
template <class Function>
inline bool
Scheduler::stepWhile(Function&& f)
{
bool ran = false;
while (f() && stepOne())
ran = true;
return ran;
}
inline bool
Scheduler::stepUntil(time_point const& until)
{
// VFALCO This routine needs optimizing
if (queue_.empty())
{
clock_.set(until);
return false;
}
auto iter = queue_.begin();
if (iter->when > until)
{
clock_.set(until);
return true;
}
do
{
stepOne();
iter = queue_.begin();
} while (iter != queue_.end() && iter->when <= until);
clock_.set(until);
return iter != queue_.end();
}
template <class Period, class Rep>
inline bool
Scheduler::stepFor(std::chrono::duration<Period, Rep> const& amount)
{
return stepUntil(now() + amount);
}
} // namespace xrpl::test::csf

171
src/tests/libxrpl/csf/Sim.h Normal file
View File

@@ -0,0 +1,171 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <csf/BasicNetwork.h>
#include <csf/CollectorRef.h>
#include <csf/Peer.h>
#include <csf/PeerGroup.h>
#include <csf/Scheduler.h>
#include <csf/SimTime.h>
#include <csf/TrustGraph.h>
#include <csf/ledgers.h>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <iostream>
#include <random>
#include <string>
#include <vector>
namespace xrpl::test::csf {
/**
* Sink that prepends simulation time to messages
*/
class BasicSink : public beast::Journal::Sink
{
Scheduler::clock_type const& clock_;
public:
BasicSink(Scheduler::clock_type const& clock)
: Sink(beast::Severity::Disabled, false), clock_{clock}
{
}
void
write(beast::Severity level, std::string const& text) override
{
if (level < threshold())
return;
std::cout << clock_.now().time_since_epoch().count() << " " << text << std::endl;
}
void
writeAlways(beast::Severity level, std::string const& text) override
{
std::cout << clock_.now().time_since_epoch().count() << " " << text << std::endl;
}
};
class Sim
{
// Use a deque to have stable pointers even when dynamically adding peers
// - Alternatively consider using unique_ptrs allocated from arena
std::deque<Peer> peers_;
PeerGroup allPeers_;
public:
std::mt19937_64 rng;
Scheduler scheduler;
BasicSink sink;
beast::Journal j;
LedgerOracle oracle;
BasicNetwork<Peer*> net;
TrustGraph<Peer*> trustGraph;
CollectorRefs collectors;
/**
* Create a simulation
*
* Creates a new simulation. The simulation has no peers, no trust links
* and no network connections.
*/
// NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test
Sim() : sink{scheduler.clock()}, j{sink}, net{scheduler}
{
}
/**
* Create a new group of peers.
*
* Creates a new group of peers. The peers do not have any trust relations
* or network connections by default. Those must be configured by the
* client.
*
* @param numPeers The number of peers in the group
* @return PeerGroup representing these new peers
*
* @note This increases the number of peers in the simulation by numPeers.
*/
PeerGroup
createGroup(std::size_t numPeers)
{
std::vector<Peer*> newPeers;
newPeers.reserve(numPeers);
for (std::size_t i = 0; i < numPeers; ++i)
{
peers_.emplace_back(
PeerID{static_cast<std::uint32_t>(peers_.size())},
scheduler,
oracle,
net,
trustGraph,
collectors,
j);
newPeers.emplace_back(&peers_.back());
}
PeerGroup res{newPeers};
allPeers_ = allPeers_ + res;
return res;
}
/**
* The number of peers in the simulation
*/
std::size_t
size() const
{
return peers_.size();
}
/**
* Run consensus protocol to generate the provided number of ledgers.
*
* Has each peer run consensus until it closes `ledgers` more ledgers.
*
* @param ledgers The number of additional ledgers to close
*/
void
run(int ledgers);
/**
* Run consensus for the given duration
*/
void
run(SimDuration const& dur);
/**
* Check whether all peers in the group are synchronized.
*
* Nodes in the group are synchronized if they share the same last
* fully validated and last generated ledger.
*/
static bool
synchronized(PeerGroup const& g);
/**
* Check whether all peers in the network are synchronized
*/
bool
synchronized() const;
/**
* Calculate the number of branches in the group.
*
* A branch occurs if two nodes in the group have fullyValidatedLedgers
* that are not on the same chain of ledgers.
*/
std::size_t
branches(PeerGroup const& g) const;
/**
* Calculate the number of branches in the network
*/
std::size_t
branches() const;
};
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,17 @@
#pragma once
#include <xrpl/beast/clock/manual_clock.h>
#include <chrono>
namespace xrpl::test::csf {
using RealClock = std::chrono::system_clock;
using RealDuration = RealClock::duration;
using RealTime = RealClock::time_point;
using SimClock = beast::ManualClock<std::chrono::steady_clock>;
using SimDuration = SimClock::duration;
using SimTime = SimClock::time_point;
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,152 @@
#pragma once
#include <boost/container/flat_set.hpp>
#include <csf/Digraph.h>
#include <algorithm>
#include <set>
#include <vector>
namespace xrpl::test::csf {
/**
* Trust graph
*
* Trust is a directed relationship from a node i to node j.
* If node i trusts node j, then node i has node j in its UNL.
* This class wraps a digraph representing the trust relationships for all
* peers in the simulation.
*/
template <class Peer>
class TrustGraph
{
using Graph = Digraph<Peer>;
Graph graph_;
public:
/**
* Create an empty trust graph
*/
TrustGraph() = default;
Graph const&
graph()
{
return graph_;
}
/**
* Create trust
*
* Establish trust between Peer `from` and Peer `to`; as if `from` put `to`
* in its UNL.
*
* @param from The peer granting trust
* @param to The peer receiving trust
*/
void
trust(Peer const& from, Peer const& to)
{
graph_.connect(from, to);
}
/**
* Remove trust
*
* Revoke trust from Peer `from` to Peer `to`; as if `from` removed `to`
* from its UNL.
*
* @param from The peer revoking trust
* @param to The peer being revoked
*/
void
untrust(Peer const& from, Peer const& to)
{
graph_.disconnect(from, to);
}
//< Whether from trusts to
[[nodiscard]] bool
trusts(Peer const& from, Peer const& to) const
{
return graph_.connected(from, to);
}
/**
* Range over trusted peers
*
* @param a The node granting trust
* @return boost transformed range over nodes `a` trusts, i.e. the nodes
* in its UNL
*/
[[nodiscard]] auto
trustedPeers(Peer const& a) const
{
return graph_.outVertices(a);
}
/**
* An example of nodes that fail the whitepaper no-forking condition
*/
struct ForkInfo
{
std::set<Peer> unlA;
std::set<Peer> unlB;
int overlap;
double required;
};
//< Return nodes that fail the white-paper no-forking condition
[[nodiscard]] std::vector<ForkInfo>
forkablePairs(double quorum) const
{
// Check the forking condition by looking at intersection
// of UNL between all pairs of nodes.
// TODO: Use the improved bound instead of the whitepaper bound.
using UNL = std::set<Peer>;
std::set<UNL> unique;
for (Peer const peer : graph_.outVertices())
{
unique.emplace(std::begin(trustedPeers(peer)), std::end(trustedPeers(peer)));
}
std::vector<UNL> uniqueUNLs(unique.begin(), unique.end());
std::vector<ForkInfo> res;
// Loop over all pairs of uniqueUNLs
for (int i = 0; i < uniqueUNLs.size(); ++i)
{
for (int j = (i + 1); j < uniqueUNLs.size(); ++j)
{
auto const& unlA = uniqueUNLs[i];
auto const& unlB = uniqueUNLs[j];
double const rhs = 2.0 * (1. - quorum) * std::max(unlA.size(), unlB.size());
int const intersectionSize = std::count_if(
unlA.begin(), unlA.end(), [&](Peer p) { return unlB.find(p) != unlB.end(); });
if (intersectionSize < rhs)
{
res.emplace_back(ForkInfo{unlA, unlB, intersectionSize, rhs});
}
}
}
return res;
}
/**
* Check whether this trust graph satisfies the whitepaper no-forking
* condition
*/
[[nodiscard]] bool
canFork(double quorum) const
{
return !forkablePairs(quorum).empty();
}
};
} // namespace xrpl::test::csf

228
src/tests/libxrpl/csf/Tx.h Normal file
View File

@@ -0,0 +1,228 @@
#pragma once
#include <xrpl/beast/hash/hash_append.h>
#include <xrpl/beast/hash/uhash.h>
#include <boost/container/flat_set.hpp>
#include <boost/iterator/function_output_iterator.hpp>
#include <algorithm>
#include <cstdint>
#include <map>
#include <ostream>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
namespace xrpl::test::csf {
/**
* A single transaction
*/
class Tx
{
public:
using ID = std::uint32_t;
Tx(ID i) : id_{i}
{
}
template <typename T>
Tx(T const* t)
requires(std::is_same_v<T, Tx>)
: id_{t->id_}
{
}
[[nodiscard]] ID const&
id() const
{
return id_;
}
bool
operator<(Tx const& o) const
{
return id_ < o.id_;
}
bool
operator==(Tx const& o) const
{
return id_ == o.id_;
}
private:
ID id_;
};
/**
* -------------------------------------------------------------------------
* All sets of Tx are represented as a flat_set for performance.
*/
using TxSetType = boost::container::flat_set<Tx>;
/**
* TxSet is a set of transactions to consider including in the ledger
*/
class TxSet
{
public:
using ID = beast::Uhash<>::result_type;
using Tx = csf::Tx;
static ID
calcID(TxSetType const& txs)
{
return beast::Uhash<>{}(txs);
}
class MutableTxSet
{
friend class TxSet;
TxSetType txs_;
public:
MutableTxSet(TxSet const& s) : txs_{s.txs_}
{
}
bool
insert(Tx const& t)
{
return txs_.insert(t).second;
}
bool
erase(Tx::ID const& txId)
{
return txs_.erase(Tx{txId}) > 0;
}
};
TxSet() = default;
TxSet(TxSetType s) : txs_{std::move(s)}, id_{calcID(txs_)}
{
}
TxSet(MutableTxSet&& m) // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
: txs_{m.txs_}, id_{calcID(txs_)}
{
}
[[nodiscard]] bool
exists(Tx::ID const txId) const
{
auto it = txs_.find(Tx{txId});
return it != txs_.end();
}
[[nodiscard]] Tx const*
find(Tx::ID const& txId) const
{
auto it = txs_.find(Tx{txId});
if (it != txs_.end())
return &(*it);
return nullptr;
}
[[nodiscard]] TxSetType const&
txs() const
{
return txs_;
}
[[nodiscard]] ID
id() const
{
return id_;
}
/**
* @return Map of Tx::ID that are missing. True means
* it was in this set and not other. False means
* it was in the other set and not this
*/
[[nodiscard]] std::map<Tx::ID, bool>
compare(TxSet const& other) const
{
std::map<Tx::ID, bool> res;
auto populateDiffs = [&res](auto const& a, auto const& b, bool s) {
auto populator = [&](auto const& tx) { res[tx.id()] = s; };
std::set_difference(
a.begin(),
a.end(),
b.begin(),
b.end(),
boost::make_function_output_iterator(std::ref(populator)));
};
populateDiffs(txs_, other.txs_, true);
populateDiffs(other.txs_, txs_, false);
return res;
}
private:
/**
* The set contains the actual transactions
*/
TxSetType txs_;
/**
* The unique ID of this tx set
*/
ID id_{};
};
//------------------------------------------------------------------------------
// Helper functions for debug printing
inline std::ostream&
operator<<(std::ostream& o, Tx const& t)
{
return o << t.id();
}
template <class T>
inline std::ostream&
operator<<(std::ostream& o, boost::container::flat_set<T> const& ts)
{
o << "{ ";
bool doComma = false;
for (auto const& t : ts)
{
if (doComma)
{
o << ", ";
}
else
{
doComma = true;
}
o << t;
}
o << " }";
return o;
}
inline std::string
to_string(TxSetType const& txs)
{
std::stringstream ss;
ss << txs;
return ss.str();
}
template <class Hasher>
inline void
hash_append(Hasher& h, Tx const& tx)
{
using beast::hash_append;
hash_append(h, tx.id());
}
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,177 @@
#pragma once
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/tagged_integer.h>
#include <csf/ledgers.h>
#include <cstdint>
#include <optional>
#include <tuple>
#include <utility>
namespace xrpl::test::csf {
struct PeerIDTag;
//< Uniquely identifies a peer
using PeerID = TaggedInteger<std::uint32_t, PeerIDTag>;
/**
* The current key of a peer
*
* Eventually, the second entry in the pair can be used to model ephemeral
* keys. Right now, the convention is to have the second entry 0 as the
* master key.
*/
using PeerKey = std::pair<PeerID, std::uint32_t>;
/**
* Validation of a specific ledger by a specific Peer.
*/
class Validation
{
Ledger::ID ledgerID_{0};
Ledger::Seq seq_{0};
NetClock::time_point signTime_;
NetClock::time_point seenTime_;
PeerKey key_;
PeerID nodeID_{0};
bool trusted_ = false;
bool full_ = false;
std::optional<std::uint32_t> loadFee_;
std::uint64_t cookie_{0};
public:
using NodeKey = PeerKey;
using NodeID = PeerID;
Validation(
Ledger::ID id,
Ledger::Seq seq,
NetClock::time_point sign,
NetClock::time_point seen,
PeerKey key,
PeerID nodeID,
bool full,
std::optional<std::uint32_t> loadFee = std::nullopt,
std::uint64_t cookie = 0)
: ledgerID_{id}
, seq_{seq}
, signTime_{sign}
, seenTime_{seen}
, key_{std::move(key)}
, nodeID_{nodeID}
, full_{full}
, loadFee_{loadFee}
, cookie_{cookie}
{
}
[[nodiscard]] Ledger::ID
ledgerID() const
{
return ledgerID_;
}
[[nodiscard]] Ledger::Seq
seq() const
{
return seq_;
}
[[nodiscard]] NetClock::time_point
signTime() const
{
return signTime_;
}
[[nodiscard]] NetClock::time_point
seenTime() const
{
return seenTime_;
}
[[nodiscard]] PeerKey const&
key() const
{
return key_;
}
[[nodiscard]] PeerID const&
nodeID() const
{
return nodeID_;
}
[[nodiscard]] bool
trusted() const
{
return trusted_;
}
[[nodiscard]] bool
full() const
{
return full_;
}
[[nodiscard]] std::uint64_t
cookie() const
{
return cookie_;
}
[[nodiscard]] std::optional<std::uint32_t>
loadFee() const
{
return loadFee_;
}
[[nodiscard]] Validation const&
unwrap() const
{
// For the xrpld implementation in which RCLValidation wraps
// STValidation, the csf::Validation has no more specific type it
// wraps, so csf::Validation unwraps to itself
return *this;
}
[[nodiscard]] auto
asTie() const
{
// trusted is a status set by the receiver, so it is not part of the tie
return std::tie(ledgerID_, seq_, signTime_, seenTime_, key_, nodeID_, loadFee_, full_);
}
bool
operator==(Validation const& o) const
{
return asTie() == o.asTie();
}
bool
operator<(Validation const& o) const
{
return asTie() < o.asTie();
}
void
setTrusted()
{
trusted_ = true;
}
void
setUntrusted()
{
trusted_ = false;
}
void
setSeen(NetClock::time_point seen)
{
seenTime_ = seen;
}
};
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,671 @@
#pragma once
#include <xrpl/basics/UnorderedContainers.h>
#include <csf/Histogram.h>
#include <csf/SimTime.h>
#include <csf/Tx.h>
#include <csf/Validation.h>
#include <csf/events.h>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstddef>
#include <iomanip>
#include <ios>
#include <map>
#include <optional>
#include <ostream>
#include <tuple>
#include <utility>
#include <vector>
namespace xrpl::test::csf {
// A collector is any class that implements
//
// on(NodeID, SimTime, Event)
//
// for all events emitted by a Peer.
//
// This file contains helper functions for composing different collectors
// and also defines several standard collectors available for simulations.
/**
* Group of collectors.
*
* Presents a group of collectors as a single collector which process an event
* by calling each collector sequentially. This is analogous to CollectorRefs
* in CollectorRef.h, but does *not* erase the type information of the combined
* collectors.
*/
template <class... Cs>
class Collectors
{
std::tuple<Cs&...> cs_;
template <class C, class E>
static void
apply(C& c, PeerID who, SimTime when, E e)
{
c.on(who, when, e);
}
template <std::size_t... Is, class E>
static void
apply(std::tuple<Cs&...>& cs, PeerID who, SimTime when, E e, std::index_sequence<Is...>)
{
(..., apply(std::get<Is>(cs), who, when, e));
}
public:
/**
* Constructor
*
* @param cs References to the collectors to call together
*/
Collectors(Cs&... cs) : cs_(std::tie(cs...))
{
}
template <class E>
void
on(PeerID who, SimTime when, E e)
{
apply(cs_, who, when, e, std::index_sequence_for<Cs...>{});
}
};
/**
* Create an instance of Collectors<Cs...>
*/
template <class... Cs>
Collectors<Cs...>
makeCollectors(Cs&... cs)
{
return Collectors<Cs...>(cs...);
}
/**
* Maintain an instance of a Collector per peer
*
* For each peer that emits events, this class maintains a corresponding
* instance of CollectorType, only forwarding events emitted by the peer to
* the related instance.
*
* CollectorType should be default constructible.
*/
template <class CollectorType>
struct CollectByNode
{
std::map<PeerID, CollectorType> byNode;
CollectorType&
operator[](PeerID who)
{
return byNode[who];
}
CollectorType const&
operator[](PeerID who) const
{
return byNode[who];
}
template <class E>
void
on(PeerID who, SimTime when, E const& e)
{
byNode[who].on(who, when, e);
}
};
/**
* Collector which ignores all events
*/
struct NullCollector
{
template <class E>
void
on(PeerID, SimTime, E const& e)
{
}
};
/**
* Tracks the overall duration of a simulation
*/
struct SimDurationCollector
{
bool init = false;
SimTime start;
SimTime stop;
template <class E>
void
on(PeerID, SimTime when, E const& e)
{
if (!init)
{
start = when;
init = true;
}
else
{
stop = when;
}
}
};
/**
* Tracks the submission -> accepted -> validated evolution of transactions.
*
* This collector tracks transactions through the network by monitoring the
* *first* time the transaction is seen by any node in the network, or
* seen by any node's accepted or fully validated ledger.
*
* If transactions submitted to the network do not have unique IDs, this
* collector will not track subsequent submissions.
*/
struct TxCollector
{
// Counts
std::size_t submitted{0};
std::size_t accepted{0};
std::size_t validated{0};
struct Tracker
{
Tx tx;
SimTime submitted;
std::optional<SimTime> accepted;
std::optional<SimTime> validated;
Tracker(Tx tx, SimTime submitted) : tx{tx}, submitted{submitted}
{
}
};
hash_map<Tx::ID, Tracker> txs;
using Hist = Histogram<SimTime::duration>;
Hist submitToAccept;
Hist submitToValidate;
// Ignore most events by default
template <class E>
void
on(PeerID, SimTime when, E const& e)
{
}
void
on(PeerID who, SimTime when, SubmitTx const& e)
{
// save first time it was seen
if (txs.emplace(e.tx.id(), Tracker{e.tx, when}).second)
{
submitted++;
}
}
void
on(PeerID who, SimTime when, AcceptLedger const& e)
{
for (auto const& tx : e.ledger.txs())
{
auto it = txs.find(tx.id());
if (it != txs.end() && !it->second.accepted)
{
Tracker& tracker = it->second;
tracker.accepted = when;
accepted++;
submitToAccept.insert(*tracker.accepted - tracker.submitted);
}
}
}
void
on(PeerID who, SimTime when, FullyValidateLedger const& e)
{
for (auto const& tx : e.ledger.txs())
{
auto it = txs.find(tx.id());
if (it != txs.end() && !it->second.validated)
{
Tracker& tracker = it->second;
// Should only validated a previously accepted Tx
assert(tracker.accepted);
tracker.validated = when;
validated++;
submitToValidate.insert(*tracker.validated - tracker.submitted);
}
}
}
// Returns the number of txs which were never accepted
[[nodiscard]] std::size_t
orphaned() const
{
return std::count_if(
txs.begin(), txs.end(), [](auto const& it) { return !it.second.accepted; });
}
// Returns the number of txs which were never validated
[[nodiscard]] std::size_t
unvalidated() const
{
return std::count_if(
txs.begin(), txs.end(), [](auto const& it) { return !it.second.validated; });
}
template <class T>
void
report(SimDuration simDuration, T& log, bool printBreakline = false)
{
using namespace std::chrono;
auto perSec = [&simDuration](std::size_t count) {
return double(count) / duration_cast<seconds>(simDuration).count();
};
auto fmtS = [](SimDuration dur) { return duration_cast<duration<float>>(dur).count(); };
if (printBreakline)
{
log << std::setw(11) << std::setfill('-') << "-" << "-" << std::setw(7)
<< std::setfill('-') << "-" << "-" << std::setw(7) << std::setfill('-') << "-"
<< "-" << std::setw(36) << std::setfill('-') << "-" << std::endl;
log << std::setfill(' ');
}
log << std::left << std::setw(11) << "TxStats" << "|" << std::setw(7) << "Count" << "|"
<< std::setw(7) << "Per Sec" << "|" << std::setw(15) << "Latency (sec)" << std::right
<< std::setw(7) << "10-ile" << std::setw(7) << "50-ile" << std::setw(7) << "90-ile"
<< std::left << std::endl;
log << std::setw(11) << std::setfill('-') << "-" << "|" << std::setw(7) << std::setfill('-')
<< "-" << "|" << std::setw(7) << std::setfill('-') << "-" << "|" << std::setw(36)
<< std::setfill('-') << "-" << std::endl;
log << std::setfill(' ');
log << std::left << std::setw(11) << "Submit " << "|" << std::right << std::setw(7)
<< submitted << "|" << std::setw(7) << std::setprecision(2) << perSec(submitted) << "|"
<< std::setw(36) << "" << std::endl;
log << std::left << std::setw(11) << "Accept " << "|" << std::right << std::setw(7)
<< accepted << "|" << std::setw(7) << std::setprecision(2) << perSec(accepted) << "|"
<< std::setw(15) << std::left << "From Submit" << std::right << std::setw(7)
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.1f)) << std::setw(7)
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.5f)) << std::setw(7)
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.9f)) << std::endl;
log << std::left << std::setw(11) << "Validate " << "|" << std::right << std::setw(7)
<< validated << "|" << std::setw(7) << std::setprecision(2) << perSec(validated) << "|"
<< std::setw(15) << std::left << "From Submit" << std::right << std::setw(7)
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.1f)) << std::setw(7)
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.5f)) << std::setw(7)
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.9f)) << std::endl;
log << std::left << std::setw(11) << "Orphan" << "|" << std::right << std::setw(7)
<< orphaned() << "|" << std::setw(7) << "" << "|" << std::setw(36) << std::endl;
log << std::left << std::setw(11) << "Unvalidated" << "|" << std::right << std::setw(7)
<< unvalidated() << "|" << std::setw(7) << "" << "|" << std::setw(43) << std::endl;
log << std::setw(11) << std::setfill('-') << "-" << "-" << std::setw(7) << std::setfill('-')
<< "-" << "-" << std::setw(7) << std::setfill('-') << "-" << "-" << std::setw(36)
<< std::setfill('-') << "-" << std::endl;
log << std::setfill(' ');
}
template <class T, class Tag>
void
csv(SimDuration simDuration, T& log, Tag const& tag, bool printHeaders = false)
{
using namespace std::chrono;
auto perSec = [&simDuration](std::size_t count) {
return double(count) / duration_cast<seconds>(simDuration).count();
};
auto fmtS = [](SimDuration dur) { return duration_cast<duration<float>>(dur).count(); };
if (printHeaders)
{
log << "tag" << "," << "txNumSubmitted" << "," << "txNumAccepted"
<< "," << "txNumValidated" << "," << "txNumOrphaned" << ","
<< "txUnvalidated" << "," << "txRateSumbitted" << ","
<< "txRateAccepted" << "," << "txRateValidated" << ","
<< "txLatencySubmitToAccept10Pctl" << ","
<< "txLatencySubmitToAccept50Pctl" << ","
<< "txLatencySubmitToAccept90Pctl" << ","
<< "txLatencySubmitToValidatet10Pctl" << ","
<< "txLatencySubmitToValidatet50Pctl" << ","
<< "txLatencySubmitToValidatet90Pctl" << std::endl;
}
log << tag
<< ","
// txNumSubmitted
<< submitted
<< ","
// txNumAccepted
<< accepted
<< ","
// txNumValidated
<< validated
<< ","
// txNumOrphaned
<< orphaned()
<< ","
// txNumUnvalidated
<< unvalidated()
<< ","
// txRateSubmitted
<< std::setprecision(2) << perSec(submitted)
<< ","
// txRateAccepted
<< std::setprecision(2) << perSec(accepted)
<< ","
// txRateValidated
<< std::setprecision(2) << perSec(validated)
<< ","
// txLatencySubmitToAccept10Pctl
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.1f))
<< ","
// txLatencySubmitToAccept50Pctl
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.5f))
<< ","
// txLatencySubmitToAccept90Pctl
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.9f))
<< ","
// txLatencySubmitToValidate10Pctl
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.1f))
<< ","
// txLatencySubmitToValidate50Pctl
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.5f))
<< ","
// txLatencySubmitToValidate90Pctl
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.9f)) << "," << std::endl;
}
};
/**
* Tracks the accepted -> validated evolution of ledgers.
*
* This collector tracks ledgers through the network by monitoring the
* *first* time the ledger is accepted or fully validated by ANY node.
*/
struct LedgerCollector
{
std::size_t accepted{0};
std::size_t fullyValidated{0};
struct Tracker
{
SimTime accepted;
std::optional<SimTime> fullyValidated;
Tracker(SimTime accepted) : accepted{accepted}
{
}
};
hash_map<Ledger::ID, Tracker> ledgers;
using Hist = Histogram<SimTime::duration>;
Hist acceptToFullyValid;
Hist acceptToAccept;
Hist fullyValidToFullyValid;
// Ignore most events by default
template <class E>
void
on(PeerID, SimTime, E const& e)
{
}
void
on(PeerID who, SimTime when, AcceptLedger const& e)
{
// First time this ledger accepted
if (ledgers.emplace(e.ledger.id(), Tracker{when}).second)
{
++accepted;
// ignore jumps?
if (e.prior.id() == e.ledger.parentID())
{
auto const it = ledgers.find(e.ledger.parentID());
if (it != ledgers.end())
{
acceptToAccept.insert(when - it->second.accepted);
}
}
}
}
void
on(PeerID who, SimTime when, FullyValidateLedger const& e)
{
// ignore jumps
if (e.prior.id() == e.ledger.parentID())
{
auto const it = ledgers.find(e.ledger.id());
assert(it != ledgers.end());
auto& tracker = it->second;
// first time fully validated
if (!tracker.fullyValidated)
{
++fullyValidated;
tracker.fullyValidated = when;
acceptToFullyValid.insert(when - tracker.accepted);
auto const parentIt = ledgers.find(e.ledger.parentID());
if (parentIt != ledgers.end())
{
auto& parentTracker = parentIt->second;
if (parentTracker.fullyValidated)
{
fullyValidToFullyValid.insert(when - *parentTracker.fullyValidated);
}
}
}
}
}
[[nodiscard]] std::size_t
unvalidated() const
{
return std::count_if(ledgers.begin(), ledgers.end(), [](auto const& it) {
return !it.second.fullyValidated;
});
}
template <class T>
void
report(SimDuration simDuration, T& log, bool printBreakline = false)
{
using namespace std::chrono;
auto perSec = [&simDuration](std::size_t count) {
return double(count) / duration_cast<seconds>(simDuration).count();
};
auto fmtS = [](SimDuration dur) { return duration_cast<duration<float>>(dur).count(); };
if (printBreakline)
{
log << std::setw(11) << std::setfill('-') << "-" << "-" << std::setw(7)
<< std::setfill('-') << "-" << "-" << std::setw(7) << std::setfill('-') << "-"
<< "-" << std::setw(36) << std::setfill('-') << "-" << std::endl;
log << std::setfill(' ');
}
log << std::left << std::setw(11) << "LedgerStats" << "|" << std::setw(7) << "Count" << "|"
<< std::setw(7) << "Per Sec"
<< "|" << std::setw(15) << "Latency (sec)" << std::right << std::setw(7) << "10-ile"
<< std::setw(7) << "50-ile" << std::setw(7) << "90-ile" << std::left << std::endl;
log << std::setw(11) << std::setfill('-') << "-" << "|" << std::setw(7) << std::setfill('-')
<< "-" << "|" << std::setw(7) << std::setfill('-') << "-" << "|" << std::setw(36)
<< std::setfill('-') << "-" << std::endl;
log << std::setfill(' ');
log << std::left << std::setw(11) << "Accept " << "|" << std::right << std::setw(7)
<< accepted << "|" << std::setw(7) << std::setprecision(2) << perSec(accepted) << "|"
<< std::setw(15) << std::left << "From Accept" << std::right << std::setw(7)
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.1f)) << std::setw(7)
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.5f)) << std::setw(7)
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.9f)) << std::endl;
log << std::left << std::setw(11) << "Validate " << "|" << std::right << std::setw(7)
<< fullyValidated << "|" << std::setw(7) << std::setprecision(2)
<< perSec(fullyValidated) << "|" << std::setw(15) << std::left << "From Validate "
<< std::right << std::setw(7) << std::setprecision(2)
<< fmtS(fullyValidToFullyValid.percentile(0.1f)) << std::setw(7) << std::setprecision(2)
<< fmtS(fullyValidToFullyValid.percentile(0.5f)) << std::setw(7) << std::setprecision(2)
<< fmtS(fullyValidToFullyValid.percentile(0.9f)) << std::endl;
log << std::setw(11) << std::setfill('-') << "-" << "-" << std::setw(7) << std::setfill('-')
<< "-" << "-" << std::setw(7) << std::setfill('-') << "-" << "-" << std::setw(36)
<< std::setfill('-') << "-" << std::endl;
log << std::setfill(' ');
}
template <class T, class Tag>
void
csv(SimDuration simDuration, T& log, Tag const& tag, bool printHeaders = false)
{
using namespace std::chrono;
auto perSec = [&simDuration](std::size_t count) {
return double(count) / duration_cast<seconds>(simDuration).count();
};
auto fmtS = [](SimDuration dur) { return duration_cast<duration<float>>(dur).count(); };
if (printHeaders)
{
log << "tag" << "," << "ledgerNumAccepted" << ","
<< "ledgerNumFullyValidated" << "," << "ledgerRateAccepted"
<< "," << "ledgerRateFullyValidated" << ","
<< "ledgerLatencyAcceptToAccept10Pctl" << ","
<< "ledgerLatencyAcceptToAccept50Pctl" << ","
<< "ledgerLatencyAcceptToAccept90Pctl" << ","
<< "ledgerLatencyFullyValidToFullyValid10Pctl" << ","
<< "ledgerLatencyFullyValidToFullyValid50Pctl" << ","
<< "ledgerLatencyFullyValidToFullyValid90Pctl" << std::endl;
}
log << tag
<< ","
// ledgerNumAccepted
<< accepted
<< ","
// ledgerNumFullyValidated
<< fullyValidated
<< ","
// ledgerRateAccepted
<< std::setprecision(2) << perSec(accepted)
<< ","
// ledgerRateFullyValidated
<< std::setprecision(2) << perSec(fullyValidated)
<< ","
// ledgerLatencyAcceptToAccept10Pctl
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.1f))
<< ","
// ledgerLatencyAcceptToAccept50Pctl
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.5f))
<< ","
// ledgerLatencyAcceptToAccept90Pctl
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.9f))
<< ","
// ledgerLatencyFullyValidToFullyValid10Pctl
<< std::setprecision(2) << fmtS(fullyValidToFullyValid.percentile(0.1f))
<< ","
// ledgerLatencyFullyValidToFullyValid50Pctl
<< std::setprecision(2) << fmtS(fullyValidToFullyValid.percentile(0.5f))
<< ","
// ledgerLatencyFullyValidToFullyValid90Pctl
<< std::setprecision(2) << fmtS(fullyValidToFullyValid.percentile(0.9f)) << std::endl;
}
};
/**
* Write out stream of ledger activity
*
* Writes information about every accepted and fully-validated ledger to a
* provided std::ostream.
*/
struct StreamCollector
{
std::ostream& out;
// Ignore most events by default
template <class E>
void
on(PeerID, SimTime, E const& e)
{
}
void
on(PeerID who, SimTime when, AcceptLedger const& e)
{
out << when.time_since_epoch().count() << ": Node " << who << " accepted " << "L"
<< e.ledger.id() << " " << e.ledger.txs() << "\n";
}
void
on(PeerID who, SimTime when, FullyValidateLedger const& e)
{
out << when.time_since_epoch().count() << ": Node " << who << " fully-validated " << "L"
<< e.ledger.id() << " " << e.ledger.txs() << "\n";
}
};
/**
* Saves information about Jumps for closed and fully validated ledgers. A
* jump occurs when a node closes/fully validates a new ledger that is not the
* immediate child of the prior closed/fully validated ledgers. This includes
* jumps across branches and jumps ahead in the same branch of ledger history.
*/
struct JumpCollector
{
struct Jump
{
PeerID id;
SimTime when;
Ledger from;
Ledger to;
};
std::vector<Jump> closeJumps;
std::vector<Jump> fullyValidatedJumps;
// Ignore most events by default
template <class E>
void
on(PeerID, SimTime, E const& e)
{
}
void
on(PeerID who, SimTime when, AcceptLedger const& e)
{
// Not a direct child -> parent switch
if (e.ledger.parentID() != e.prior.id())
closeJumps.emplace_back(Jump{.id = who, .when = when, .from = e.prior, .to = e.ledger});
}
void
on(PeerID who, SimTime when, FullyValidateLedger const& e)
{
// Not a direct child -> parent switch
if (e.ledger.parentID() != e.prior.id())
{
fullyValidatedJumps.emplace_back(
Jump{.id = who, .when = when, .from = e.prior, .to = e.ledger});
}
}
};
} // namespace xrpl::test::csf

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -0,0 +1,157 @@
#pragma once
#include <csf/Tx.h>
#include <csf/Validation.h>
#include <csf/ledgers.h>
namespace xrpl::test::csf {
// Events are emitted by peers at a variety of points during the simulation.
// Each event is emitted by a particular peer at a particular time. Collectors
// process these events, perhaps calculating statistics or storing events to
// a log for post-processing.
//
// The Event types can be arbitrary, but should be copyable and lightweight.
//
// Example collectors can be found in collectors.h, but have the general
// interface:
//
// @code
// template <class T>
// struct Collector
// {
// template <class Event>
// void
// on(peerID who, SimTime when, Event e);
// };
// @endcode
//
// CollectorRef.f defines a type-erased holder for arbitrary Collectors. If
// any new events are added, the interface there needs to be updated.
/**
* A value to be flooded to all other peers starting from this peer.
*/
template <class V>
struct Share
{
/**
* Event that is shared
*/
V val;
};
/**
* A value relayed to another peer as part of flooding
*/
template <class V>
struct Relay
{
/**
* Peer relaying to
*/
PeerID to;
/**
* The value to relay
*/
V val;
};
/**
* A value received from another peer as part of flooding
*/
template <class V>
struct Receive
{
/**
* Peer that sent the value
*/
PeerID from;
/**
* The received value
*/
V val;
};
/**
* A transaction submitted to a peer
*/
struct SubmitTx
{
/**
* The submitted transaction
*/
Tx tx;
};
/**
* Peer starts a new consensus round
*/
struct StartRound
{
/**
* The preferred ledger for the start of consensus
*/
Ledger::ID bestLedger{};
/**
* The prior ledger on hand
*/
Ledger prevLedger;
};
/**
* Peer closed the open ledger
*/
struct CloseLedger
{
// The ledger closed on
Ledger prevLedger;
// Initial txs for including in ledger
TxSetType txs;
};
/**
* Peer accepted consensus results
*/
struct AcceptLedger
{
// The newly created ledger
Ledger ledger;
// The prior ledger (this is a jump if prior.id() != ledger.parentID())
Ledger prior;
};
/**
* Peer detected a wrong prior ledger during consensus
*/
struct WrongPrevLedger
{
// ID of wrong ledger we had
Ledger::ID wrong;
// ID of what we think is the correct ledger
Ledger::ID right;
};
/**
* Peer fully validated a new ledger
*/
struct FullyValidateLedger
{
/**
* The new fully validated ledger
*/
Ledger ledger;
/**
* The prior fully validated ledger
* This is a jump if prior.id() != ledger.parentID()
*/
Ledger prior;
};
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,70 @@
#include <csf/Sim.h>
#include <csf/PeerGroup.h>
#include <csf/SimTime.h>
#include <algorithm>
#include <cstddef>
#include <limits>
#include <set>
namespace xrpl::test::csf {
void
Sim::run(int ledgers)
{
for (auto& p : peers_)
{
p.targetLedgers = p.completedLedgers + ledgers;
p.start();
}
scheduler.step();
}
void
Sim::run(SimDuration const& dur)
{
for (auto& p : peers_)
{
p.targetLedgers = std::numeric_limits<decltype(p.targetLedgers)>::max();
p.start();
}
scheduler.stepFor(dur);
}
bool
Sim::synchronized() const
{
return synchronized(allPeers_);
}
bool
Sim::synchronized(PeerGroup const& g)
{
if (g.size() < 1)
return true;
Peer const* ref = g[0];
return std::ranges::all_of(g, [&ref](Peer const* p) {
return p->lastClosedLedger.id() == ref->lastClosedLedger.id() &&
p->fullyValidatedLedger.id() == ref->fullyValidatedLedger.id();
});
}
std::size_t
Sim::branches() const
{
return branches(allPeers_);
}
std::size_t
Sim::branches(PeerGroup const& g) const
{
if (g.size() < 1)
return 0;
std::set<Ledger> ledgers;
for (auto const& peer : g)
ledgers.insert(peer->fullyValidatedLedger);
return oracle.branches(ledgers);
}
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,167 @@
#include <csf/ledgers.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/LedgerTiming.h>
#include <csf/Tx.h>
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <optional>
#include <set>
#include <vector>
namespace xrpl::test::csf {
Ledger::Instance const Ledger::kGenesis;
json::Value
Ledger::getJson() const
{
json::Value res(json::ValueType::Object);
res["id"] = static_cast<ID::value_type>(id());
res["seq"] = static_cast<Seq::value_type>(seq());
return res;
}
bool
Ledger::isAncestor(Ledger const& ancestor) const
{
if (ancestor.seq() < seq())
return operator[](ancestor.seq()) == ancestor.id();
return false;
}
Ledger::ID
Ledger::operator[](Seq s) const
{
if (s > seq())
return {};
if (s == seq())
return id();
return instance_->ancestors[static_cast<Seq::value_type>(s)];
}
Ledger::Seq
mismatch(Ledger const& a, Ledger const& b)
{
using Seq = Ledger::Seq;
// end is 1 past end of range
Seq start{0};
Seq const end = std::min(a.seq() + Seq{1}, b.seq() + Seq{1});
// Find mismatch in [start,end)
// Binary search
Seq count = end - start;
while (count > Seq{0})
{
Seq const step = count / Seq{2};
Seq curr = start + step;
if (a[curr] == b[curr])
{
// go to second half
start = ++curr;
count -= step + Seq{1};
}
else
{
count = step;
}
}
return start;
}
LedgerOracle::LedgerOracle()
{
instances_.insert(InstanceEntry{Ledger::kGenesis, nextID()});
}
Ledger::ID
LedgerOracle::nextID() const
{
return Ledger::ID{static_cast<Ledger::ID::value_type>(instances_.size())};
}
Ledger
LedgerOracle::accept(
Ledger const& parent,
TxSetType const& txs,
NetClock::duration closeTimeResolution,
NetClock::time_point const& consensusCloseTime)
{
using namespace std::chrono_literals;
Ledger::Instance next(*parent.instance_);
next.txs.insert(txs.begin(), txs.end());
next.seq = parent.seq() + Ledger::Seq{1};
next.closeTimeResolution = closeTimeResolution;
next.closeTimeAgree = consensusCloseTime != NetClock::time_point{};
if (next.closeTimeAgree)
{
next.closeTime = effCloseTime(consensusCloseTime, closeTimeResolution, parent.closeTime());
}
else
{
next.closeTime = parent.closeTime() + 1s;
}
next.parentCloseTime = parent.closeTime();
next.parentID = parent.id();
next.ancestors.push_back(parent.id());
auto it = instances_.left.find(next);
if (it == instances_.left.end())
{
using Entry = InstanceMap::left_value_type;
it = instances_.left.insert(Entry{next, nextID()}).first;
}
return Ledger(it->second, &(it->first));
}
std::optional<Ledger>
LedgerOracle::lookup(Ledger::ID const& id) const
{
auto const it = instances_.right.find(id);
if (it != instances_.right.end())
{
return Ledger(it->first, &(it->second));
}
return std::nullopt;
}
std::size_t
LedgerOracle::branches(std::set<Ledger> const& ledgers)
{
// Tips always maintains the Ledgers with largest sequence number
// along all known chains.
std::vector<Ledger> tips;
tips.reserve(ledgers.size());
for (Ledger const& ledger : ledgers)
{
// Three options,
// 1. ledger is on a new branch
// 2. ledger is on a branch that we have seen tip for
// 3. ledger is the new tip for a branch
bool found = false;
for (auto idx = 0; idx < tips.size() && !found; ++idx)
{
bool const idxEarlier = tips[idx].seq() < ledger.seq();
Ledger const& earlier = idxEarlier ? tips[idx] : ledger;
Ledger const& later = idxEarlier ? ledger : tips[idx];
if (later.isAncestor(earlier))
{
tips[idx] = later;
found = true;
}
}
if (!found)
tips.push_back(ledger);
}
// The size of tips is the number of branches
return tips.size();
}
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,360 @@
#pragma once
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/tagged_integer.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/LedgerTiming.h>
#include <boost/bimap/bimap.hpp>
#include <csf/Tx.h>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <optional>
#include <set>
#include <string>
#include <tuple>
#include <unordered_map>
#include <vector>
namespace xrpl::test::csf {
/**
* A ledger is a set of observed transactions and a sequence number
* identifying the ledger.
*
* Peers in the consensus process are trying to agree on a set of transactions
* to include in a ledger. For simulation, each transaction is a single
* integer and the ledger is the set of observed integers. This means future
* ledgers have prior ledgers as subsets, e.g.
*
* Ledger 0 : {}
* Ledger 1 : {1,4,5}
* Ledger 2 : {1,2,4,5,10}
* ....
*
* Ledgers are immutable value types. All ledgers with the same sequence
* number, transactions, close time, etc. will have the same ledger ID. The
* LedgerOracle class below manages ID assignments for a simulation and is the
* only way to close and create a new ledger. Since the parent ledger ID is
* part of type, this also means ledgers with distinct histories will have
* distinct ids, even if they have the same set of transactions, sequence
* number and close time.
*/
class Ledger
{
friend class LedgerOracle;
public:
struct SeqTag;
using Seq = TaggedInteger<std::uint32_t, SeqTag>;
struct IdTag;
using ID = TaggedInteger<std::uint32_t, IdTag>;
struct MakeGenesis
{
};
private:
// The instance is the common immutable data that will be assigned a unique
// ID by the oracle
struct Instance
{
Instance() = default;
// Sequence number
Seq seq{0};
// Transactions added to generate this ledger
TxSetType txs;
// Resolution used to determine close time
NetClock::duration closeTimeResolution = kLedgerDefaultTimeResolution;
/**
* When the ledger closed (up to closeTimeResolution)
*/
NetClock::time_point closeTime;
/**
* Whether consensus agreed on the close time
*/
bool closeTimeAgree = true;
/**
* Parent ledger id
*/
ID parentID{0};
/**
* Parent ledger close time
*/
NetClock::time_point parentCloseTime;
/**
* IDs of this ledgers ancestors. Since each ledger already has unique
* ancestors based on the parentID, this member is not needed for any
* of the operators below.
*/
std::vector<Ledger::ID> ancestors;
[[nodiscard]] auto
asTie() const
{
return std::tie(
seq,
txs,
closeTimeResolution,
closeTime,
closeTimeAgree,
parentID,
parentCloseTime);
}
friend bool
operator==(Instance const& a, Instance const& b)
{
return a.asTie() == b.asTie();
}
friend bool
operator!=(Instance const& a, Instance const& b)
{
return a.asTie() != b.asTie();
}
friend bool
operator<(Instance const& a, Instance const& b)
{
return a.asTie() < b.asTie();
}
template <class Hasher>
friend void
hash_append(
Hasher& h,
Ledger::Instance const& instance) // NOLINT(readability-identifier-naming)
{
using beast::hash_append;
hash_append(h, instance.asTie());
}
};
// Single common genesis instance
static Instance const kGenesis;
Ledger(ID id, Instance const* i) : id_{id}, instance_{i}
{
}
public:
Ledger(MakeGenesis) : instance_(&kGenesis)
{
}
// This is required by the generic Consensus for now and should be
// migrated to the MakeGenesis approach above.
Ledger() : Ledger(MakeGenesis{})
{
}
[[nodiscard]] ID
id() const
{
return id_;
}
[[nodiscard]] Seq
seq() const
{
return instance_->seq;
}
[[nodiscard]] NetClock::duration
closeTimeResolution() const
{
return instance_->closeTimeResolution;
}
[[nodiscard]] bool
closeAgree() const
{
return instance_->closeTimeAgree;
}
[[nodiscard]] NetClock::time_point
closeTime() const
{
return instance_->closeTime;
}
[[nodiscard]] NetClock::time_point
parentCloseTime() const
{
return instance_->parentCloseTime;
}
[[nodiscard]] ID
parentID() const
{
return instance_->parentID;
}
[[nodiscard]] TxSetType const&
txs() const
{
return instance_->txs;
}
/**
* Determine whether ancestor is really an ancestor of this ledger
*/
[[nodiscard]] bool
isAncestor(Ledger const& ancestor) const;
/**
* Return the id of the ancestor with the given seq (if exists/known)
*/
ID
operator[](Seq seq) const;
/**
* Return the sequence number of the first mismatching ancestor
*/
friend Ledger::Seq
mismatch(Ledger const& a, Ledger const& o);
[[nodiscard]] json::Value
getJson() const;
friend bool
operator<(Ledger const& a, Ledger const& b)
{
return a.id() < b.id();
}
private:
ID id_{0};
Instance const* instance_;
};
/**
* Oracle maintaining unique ledgers for a simulation.
*/
class LedgerOracle
{
using InstanceMap = boost::bimaps::bimap<
boost::bimaps::set_of<Ledger::Instance, std::less<>>,
boost::bimaps::set_of<Ledger::ID, std::less<>>>;
using InstanceEntry = InstanceMap::value_type;
// Set of all known ledgers; note this is never pruned
InstanceMap instances_;
// ID for the next unique ledger
[[nodiscard]] Ledger::ID
nextID() const;
public:
LedgerOracle();
/**
* Find the ledger with the given ID
*/
[[nodiscard]] std::optional<Ledger>
lookup(Ledger::ID const& id) const;
/**
* Accept the given txs and generate a new ledger
*
* @param curr The current ledger
* @param txs The transactions to apply to the current ledger
* @param closeTimeResolution Resolution used in determining close time
* @param consensusCloseTime The consensus agreed close time, no valid time
* if 0
*/
Ledger
accept(
Ledger const& curr,
TxSetType const& txs,
NetClock::duration closeTimeResolution,
NetClock::time_point const& consensusCloseTime);
Ledger
accept(Ledger const& curr, Tx tx)
{
using namespace std::chrono_literals;
return accept(curr, TxSetType{tx}, curr.closeTimeResolution(), curr.closeTime() + 1s);
}
/**
* Determine the number of distinct branches for the set of ledgers.
*
* Ledgers A and B are on different branches if A != B, A is not an
* ancestor of B and B is not an ancestor of A, e.g.
*
* /--> A
* O
* \--> B
*/
static std::size_t
branches(std::set<Ledger> const& ledgers);
};
/**
* Helper for writing unit tests with controlled ledger histories.
*
* This class allows clients to refer to distinct ledgers as strings, where
* each character in the string indicates a unique ledger. It enforces the
* uniqueness at runtime, but this simplifies creation of alternate ledger
* histories, e.g.
*
* HistoryHelper hh;
* hh["a"]
* hh["ab"]
* hh["ac"]
* hh["abd"]
*
* Creates a history like
* b - d
* /
* a - c
*/
struct LedgerHistoryHelper
{
LedgerOracle oracle;
Tx::ID nextTx{0};
std::unordered_map<std::string, Ledger> ledgers;
std::set<char> seen;
LedgerHistoryHelper()
{
ledgers[""] = Ledger{Ledger::MakeGenesis{}};
}
/**
* Get or create the ledger with the given string history.
*
* Creates any necessary intermediate ledgers, but asserts if
* a letter is re-used (e.g. "abc" then "adc" would assert)
*/
Ledger const&
operator[](std::string const& s)
{
auto it = ledgers.find(s);
if (it != ledgers.end())
return it->second;
// enforce that the new suffix has never been seen
assert(seen.emplace(s.back()).second);
Ledger const& parent = (*this)[s.substr(0, s.size() - 1)];
return ledgers.emplace(s, oracle.accept(parent, Tx{++nextTx})).first->second;
}
};
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,158 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <iterator>
#include <random>
#include <vector>
namespace xrpl::test::csf {
/**
* Return a randomly shuffled copy of vector based on weights w.
*
* @param v The set of values
* @param w The set of weights of each value
* @param g A pseudo-random number generator
* @return A vector with entries randomly sampled without replacement
* from the original vector based on the provided weights.
* I.e. res[0] comes from sample v[i] with weight w[i]/suk_ w[k]
*/
template <class T, class G>
std::vector<T>
randomWeightedShuffle(std::vector<T> v, std::vector<double> w, G& g)
{
using std::swap;
for (int i = 0; i < v.size() - 1; ++i)
{
// pick a random item weighted by w
std::discrete_distribution<> dd(w.begin() + i, w.end()); // NOLINT(misc-const-correctness)
auto idx = dd(g);
std::swap(v[i], v[idx]);
std::swap(w[i], w[idx]);
}
return v;
}
/**
* Generate a vector of random samples
*
* @param size the size of the sample
* @param dist the distribution to sample
* @param g the pseudo-random number generator
*
* @return vector of samples
*/
template <class RandomNumberDistribution, class Generator>
std::vector<typename RandomNumberDistribution::result_type>
sample(std::size_t size, RandomNumberDistribution dist, Generator& g)
{
std::vector<typename RandomNumberDistribution::result_type> res(size);
std::ranges::generate(res, [&dist, &g]() { return dist(g); });
return res;
}
/**
* Invocable that returns random samples from a range according to a discrete
* distribution
*
* Given a pair of random access iterators begin and end, each call to the
* instance of Selector returns a random entry in the range (begin,end)
* according to the weights provided at construction.
*/
template <class RAIter, class Generator>
class Selector
{
RAIter first_, last_;
std::discrete_distribution<> dd_;
Generator g_;
public:
/**
* Constructor
* @param first Random access iterator to the start of the range
* @param last Random access iterator to the end of the range
* @param w Vector of weights of size list-first
* @param g the pseudo-random number generator
*/
Selector(RAIter first, RAIter last, std::vector<double> const& w, Generator& g)
: first_{first}, last_{last}, dd_{w.begin(), w.end()}, g_{g}
{
using tag = std::iterator_traits<RAIter>::iterator_category;
static_assert(
std::is_same_v<tag, std::random_access_iterator_tag>,
"Selector only supports random access iterators.");
// TODO: Allow for forward iterators
}
std::iterator_traits<RAIter>::value_type
operator()()
{
auto idx = dd_(g_);
return *(first_ + idx);
}
};
template <typename Iter, typename Generator>
Selector<Iter, Generator>
makeSelector(Iter first, Iter last, std::vector<double> const& w, Generator& g)
{
return Selector<Iter, Generator>(first, last, w, g);
}
//------------------------------------------------------------------------------
// Additional distributions of interest not defined in <random>
/**
* Constant "distribution" that always returns the same value
*/
class ConstantDistribution
{
double t_;
public:
ConstantDistribution(double const& t) : t_{t}
{
}
template <class Generator>
double
operator()(Generator&)
{
return t_;
}
};
/**
* Power-law distribution with PDF
*
* P(x) = (x/xmin)^-a
*
* for a >= 1 and xmin >= 1
*/
class PowerLawDistribution
{
double xmin_;
double a_;
double inv_{1.0 / (1.0 - a_)};
std::uniform_real_distribution<double> uf_{0, 1};
public:
using result_type = double;
PowerLawDistribution(double xmin, double a) : xmin_{xmin}, a_{a}
{
}
template <class Generator>
double
operator()(Generator& g)
{
// use inverse transform of CDF to sample
// CDF is P(X <= x): 1 - (x/xmin)^(1-a)
return xmin_ * std::pow(1 - uf_(g), inv_);
}
};
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,111 @@
#pragma once
#include <csf/Scheduler.h>
#include <csf/SimTime.h>
#include <csf/Tx.h>
#include <cstddef>
#include <cstdint>
#include <type_traits>
namespace xrpl::test::csf {
// Submitters are classes for simulating submission of transactions to the
// network
/**
* Represents rate as a count/duration
*/
struct Rate
{
std::size_t count;
SimDuration duration;
[[nodiscard]] double
inv() const
{
return duration.count() / double(count);
}
};
/**
* Submits transactions to a specified peer
*
* Submits successive transactions beginning at start, then spaced according
* to successive calls of distribution(), until stop.
*
* @tparam Distribution is a `UniformRandomBitGenerator` from the STL that
* is used by random distributions to generate random samples
* @tparam Generator is an object with member
*
* T operator()(Generator &g)
*
* which generates the delay T in SimDuration units to the next
* transaction. For the current definition of SimDuration, this is
* currently the number of nanoseconds. Submitter internally casts
* arithmetic T to SimDuration::rep units to allow using standard
* library distributions as a Distribution.
*/
template <class Distribution, class Generator, class Selector>
class Submitter
{
Distribution dist_;
SimTime stop_;
std::uint32_t nextID_ = 0;
Selector selector_;
Scheduler& scheduler_;
Generator& g_;
// Convert generated durations to SimDuration
static SimDuration
asDuration(SimDuration d)
{
return d;
}
template <class T>
static SimDuration
asDuration(T t)
requires(std::is_arithmetic_v<T>)
{
return SimDuration{static_cast<SimDuration::rep>(t)};
}
void
submit()
{
selector_()->submit(Tx{nextID_++});
if (scheduler_.now() < stop_)
{
scheduler_.in(asDuration(dist_(g_)), [&]() { submit(); });
}
}
public:
Submitter(
Distribution dist,
SimTime start,
SimTime end,
Selector& selector,
Scheduler& s,
Generator& g)
: dist_{dist}, stop_{end}, selector_{selector}, scheduler_{s}, g_{g}
{
scheduler_.at(start, [&]() { submit(); });
}
};
template <class Distribution, class Generator, class Selector>
Submitter<Distribution, Generator, Selector>
makeSubmitter(
Distribution dist,
SimTime start,
SimTime end,
Selector& sel,
Scheduler& s,
Generator& g)
{
return Submitter<Distribution, Generator, Selector>(dist, start, end, sel, s, g);
}
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,63 @@
#pragma once
#include <csf/Scheduler.h>
#include <csf/SimTime.h>
#include <chrono>
#include <iostream>
#include <ostream>
namespace xrpl::test::csf {
// Timers are classes that schedule repeated events and are mostly independent
// of simulation-specific details.
/**
* Gives heartbeat of simulation to signal simulation progression
*/
class HeartbeatTimer
{
Scheduler& scheduler_;
SimDuration interval_;
std::ostream& out_;
RealTime startRealTime_;
SimTime startSimTime_;
public:
HeartbeatTimer(
Scheduler& sched,
SimDuration interval = std::chrono::seconds{60},
std::ostream& out = std::cerr)
: scheduler_{sched}
, interval_{interval}
, out_{out}
, startRealTime_{RealClock::now()}
, startSimTime_{sched.now()}
{
}
void
start()
{
scheduler_.in(interval_, [this]() { beat(scheduler_.now()); });
}
void
beat(SimTime when)
{
using namespace std::chrono;
RealTime const realTime = RealClock::now();
SimTime const simTime = when;
RealDuration const realDuration = realTime - startRealTime_;
SimDuration const simDuration = simTime - startSimTime_;
out_ << "Heartbeat. Time Elapsed: {sim: " << duration_cast<seconds>(simDuration).count()
<< "s | real: " << duration_cast<seconds>(realDuration).count() << "s}\n"
<< std::flush;
scheduler_.in(interval_, [this]() { beat(scheduler_.now()); });
}
};
} // namespace xrpl::test::csf

View File

@@ -0,0 +1,50 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <mutex>
#include <sstream>
#include <string>
namespace xrpl::test {
class CaptureSink : public beast::Journal::Sink
{
mutable std::mutex mutex_;
std::stringstream strm_;
public:
explicit CaptureSink(beast::Severity threshold = beast::Severity::Debug)
: Sink{threshold, false}
{
}
void
write(beast::Severity level, std::string const& text) override
{
if (level < threshold())
return;
writeAlways(level, text);
}
void
writeAlways(beast::Severity /*level*/, std::string const& text) override
{
// Journal sinks may be written to concurrently (e.g. from a backend's background workers),
// so serialize access to strm_. write() funnels into writeAlways(), so the lock lives here
// only: locking in both would self-deadlock on this non-recursive mutex.
std::scoped_lock const lock(mutex_);
strm_ << text << '\n';
}
[[nodiscard]] std::string
messages() const
{
// Returns a snapshot of the captured output. Takes the lock so the read is safe even if a
// writer is still active.
std::scoped_lock const lock(mutex_);
return strm_.str();
}
};
} // namespace xrpl::test

View File

@@ -29,11 +29,11 @@ namespace xrpl::test {
class TestFamily : public Family
{
private:
std::unique_ptr<NodeStore::Database> db_;
std::unique_ptr<node_store::Database> db_;
TestStopwatch clock_;
std::shared_ptr<FullBelowCache> fbCache_;
std::shared_ptr<TreeNodeCache> tnCache_;
NodeStore::DummyScheduler scheduler_;
node_store::DummyScheduler scheduler_;
beast::Journal j_;
public:
@@ -51,16 +51,16 @@ public:
Section config;
config.set(Keys::kType, "memory");
config.set(Keys::kPath, "TestFamily");
db_ = NodeStore::Manager::instance().makeDatabase(megabytes(4), scheduler_, 1, config, j);
db_ = node_store::Manager::instance().makeDatabase(megabytes(4), scheduler_, 1, config, j);
}
NodeStore::Database&
node_store::Database&
db() override
{
return *db_;
}
[[nodiscard]] NodeStore::Database const&
[[nodiscard]] node_store::Database const&
db() const override
{
return *db_;

View File

@@ -221,7 +221,7 @@ public:
}
// Storage services
NodeStore::Database&
node_store::Database&
getNodeStore() override
{
throw std::logic_error("TestServiceRegistry::getNodeStore() not implemented");

View File

@@ -1,8 +1,9 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
int
main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@@ -0,0 +1,182 @@
#include <xrpl/nodestore/Backend.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/nodestore/DummyScheduler.h>
#include <xrpl/nodestore/Manager.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Types.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <nodestore/TestBase.h>
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <memory>
#include <ranges>
#include <string>
#include <thread>
#include <vector>
namespace xrpl::node_store {
namespace {
std::vector<std::string>
backendTypes()
{
std::vector<std::string> types{"nudb"};
#if XRPL_ROCKSDB_AVAILABLE
types.emplace_back("rocksdb");
#endif
#ifdef XRPL_ENABLE_SQLITE_BACKEND_TESTS
types.emplace_back("sqlite");
#endif
return types;
}
// Run work(i) for every i in [0, n) spread across numThreads threads, handing
// out indices via a shared atomic counter (mirrors the old Timing_test
// parallel-for so the N items are partitioned, not duplicated).
template <class Work>
void
parallelFor(std::size_t n, std::size_t numThreads, Work work)
{
std::atomic<std::size_t> next{0};
auto const runner = [&] {
for (std::size_t i = next++; i < n; i = next++)
work(i);
};
auto threads = std::views::iota(std::size_t{0}, numThreads) |
std::views::transform([&](std::size_t) { return std::thread{runner}; }) |
std::ranges::to<std::vector>();
std::ranges::for_each(threads, &std::thread::join);
}
} // namespace
class BackendTypeTest : public ::testing::TestWithParam<std::string>
{
protected:
void
SetUp() override
{
params_.set("type", GetParam());
params_.set("path", tempDir_.path());
beast::xor_shift_engine rng(kSeedValue);
batch_ = createPredictableBatch(kNumObjects, rng());
}
std::unique_ptr<Backend>
makeOpenBackend()
{
auto backend = Manager::instance().makeBackend(params_, megabytes(4), scheduler_, journal_);
backend->open();
return backend;
}
DummyScheduler scheduler_;
beast::TempDir const tempDir_;
beast::Journal const journal_{TestSink::instance()};
Section params_;
Batch batch_;
};
TEST_P(BackendTypeTest, store_and_fetch)
{
auto backend = makeOpenBackend();
storeBatch(*backend, batch_);
{
SCOPED_TRACE("read in original order");
auto const copy = fetchCopyOfBatch(*backend, batch_);
EXPECT_EQ(batch_, copy);
}
{
SCOPED_TRACE("read in shuffled order");
beast::xor_shift_engine rng(kSeedValue);
std::shuffle(batch_.begin(), batch_.end(), rng);
auto const copy = fetchCopyOfBatch(*backend, batch_);
EXPECT_EQ(batch_, copy);
}
}
TEST_P(BackendTypeTest, persists_after_reopen)
{
{
auto backend = makeOpenBackend();
storeBatch(*backend, batch_);
}
// re-open a fresh backend instance over the same path
auto backend = makeOpenBackend();
auto copy = fetchCopyOfBatch(*backend, batch_);
std::ranges::sort(batch_, LessThan{});
std::ranges::sort(copy, LessThan{});
EXPECT_EQ(batch_, copy);
}
// missing-key path. Replaces the correctness half of Timing_test::doMissing
// (and the missing branch of doMixed): every fetch on an empty backend must
// report Status::NotFound.
TEST_P(BackendTypeTest, fetch_missing)
{
auto backend = makeOpenBackend();
// deliberately do NOT store batch_ — every key must be absent
fetchMissing(*backend, batch_);
}
// concurrent store/fetch correctness. Replaces the correctness half of the
// multi-threaded Timing_test workloads (which only ran manually, never in CI):
// many threads store disjoint objects, then many threads fetch and verify each
// round-trips. Doubles as a thread-safety smoke test for the backend.
TEST_P(BackendTypeTest, concurrent_store_and_fetch)
{
// The SQLite backend is not designed for concurrent writers (and the old
// Timing_test only exercised nudb/rocksdb under threads).
if (GetParam() == "sqlite")
GTEST_SKIP() << "sqlite backend is not exercised under concurrency";
for (auto const numThreads : {4uz, 8uz})
{
SCOPED_TRACE("threads=" + std::to_string(numThreads));
auto backend = makeOpenBackend();
// concurrent stores of disjoint objects
parallelFor(batch_.size(), numThreads, [&](std::size_t i) { backend->store(batch_[i]); });
// concurrent fetches, each verifying its object round-trips. Worker
// threads only touch an atomic counter; the EXPECT runs on the main
// thread after join to avoid relying on cross-thread assertion support.
std::atomic<std::size_t> mismatches{0};
parallelFor(batch_.size(), numThreads, [&](std::size_t i) {
std::shared_ptr<NodeObject> result;
if (backend->fetch(batch_[i]->getHash(), &result) != Status::Ok || !result ||
!isSame(result, batch_[i]))
{
++mismatches;
}
});
EXPECT_EQ(mismatches.load(), 0u);
backend->close();
}
}
INSTANTIATE_TEST_SUITE_P(
BackendTypes,
BackendTypeTest,
::testing::ValuesIn(backendTypes()),
[](::testing::TestParamInfo<std::string> const& info) { return info.param; });
} // namespace xrpl::node_store

View File

@@ -0,0 +1,41 @@
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/detail/DecodedBlob.h>
#include <xrpl/nodestore/detail/EncodedBlob.h>
#include <gtest/gtest.h>
#include <nodestore/TestBase.h>
#include <cstddef>
#include <memory>
#include <string>
namespace xrpl::node_store {
TEST(NodeStoreBasics, predictable_batches)
{
auto const batch1 = createPredictableBatch(kNumObjectsToTest, kSeedValue);
auto const batch2 = createPredictableBatch(kNumObjectsToTest, kSeedValue);
EXPECT_EQ(batch1, batch2);
auto const batch3 = createPredictableBatch(kNumObjectsToTest, kSeedValue + 1);
EXPECT_NE(batch1, batch3);
}
TEST(NodeStoreBasics, blob_encoding)
{
auto const batch = createPredictableBatch(kNumObjectsToTest, kSeedValue);
for (std::size_t i = 0; i < batch.size(); ++i)
{
SCOPED_TRACE("blob index=" + std::to_string(i));
EncodedBlob const encoded(batch[i]);
DecodedBlob decoded(encoded.getKey(), encoded.getData(), encoded.getSize());
EXPECT_TRUE(decoded.wasOk());
if (decoded.wasOk())
{
std::shared_ptr<NodeObject> const object(decoded.createObject());
EXPECT_TRUE(isSame(batch[i], object));
}
}
}
} // namespace xrpl::node_store

View File

@@ -0,0 +1,248 @@
#include <xrpl/nodestore/Database.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/nodestore/DummyScheduler.h>
#include <xrpl/nodestore/Manager.h>
#include <xrpl/nodestore/Types.h>
#include <xrpl/protocol/SystemParameters.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <nodestore/TestBase.h>
#include <algorithm>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
namespace xrpl::node_store {
namespace {
std::vector<std::string>
allBackends()
{
std::vector<std::string> types{"memory", "nudb"};
#if XRPL_ROCKSDB_AVAILABLE
types.emplace_back("rocksdb");
#endif
return types;
}
std::vector<std::string>
persistentBackends()
{
std::vector<std::string> types{"nudb"};
#if XRPL_ROCKSDB_AVAILABLE
types.emplace_back("rocksdb");
#endif
return types;
}
std::vector<std::string>
importBackends()
{
std::vector<std::string> types{"nudb"};
#if XRPL_ROCKSDB_AVAILABLE
types.emplace_back("rocksdb");
#endif
#ifdef XRPL_ENABLE_SQLITE_BACKEND_TESTS
types.emplace_back("sqlite");
#endif
return types;
}
} // namespace
// Shared setup for the parameterized Database tests: builds the node params,
// journal and a predictable batch per test, mirroring Backend.cpp's fixture.
class NodeStoreDatabaseTestBase : public ::testing::TestWithParam<std::string>
{
protected:
void
SetUp() override
{
nodeParams_.set("type", GetParam());
nodeParams_.set("path", nodeDb_.path());
beast::xor_shift_engine rng(kSeedValue);
batch_ = createPredictableBatch(kNumObjects, rng());
}
std::unique_ptr<Database>
makeDatabase()
{
return Manager::instance().makeDatabase(megabytes(4), scheduler_, 2, nodeParams_, journal_);
}
DummyScheduler scheduler_;
beast::TempDir const nodeDb_;
beast::Journal const journal_{TestSink::instance()};
Section nodeParams_;
Batch batch_;
};
class NodeStoreDatabaseTest : public NodeStoreDatabaseTestBase
{
};
class NodeStoreDatabasePersistenceTest : public NodeStoreDatabaseTestBase
{
};
TEST_P(NodeStoreDatabaseTest, store_and_fetch)
{
auto db = makeDatabase();
storeBatch(*db, batch_);
{
SCOPED_TRACE("read in original order");
auto const copy = fetchCopyOfBatch(*db, batch_);
EXPECT_EQ(batch_, copy);
}
{
SCOPED_TRACE("read in shuffled order");
beast::xor_shift_engine rng(kSeedValue);
std::shuffle(batch_.begin(), batch_.end(), rng);
auto const copy = fetchCopyOfBatch(*db, batch_);
EXPECT_EQ(batch_, copy);
}
}
TEST_P(NodeStoreDatabasePersistenceTest, round_trip)
{
{
auto db = makeDatabase();
storeBatch(*db, batch_);
}
// re-open without the ephemeral db
auto db = makeDatabase();
auto copy = fetchCopyOfBatch(*db, batch_);
std::ranges::sort(batch_, LessThan{});
std::ranges::sort(copy, LessThan{});
EXPECT_EQ(batch_, copy);
}
// missing-key path at the Database layer. Mirrors Backend's fetch_missing —
// fetching keys that were never stored must return nullptr (NotFound).
TEST_P(NodeStoreDatabaseTest, fetch_missing)
{
auto db = makeDatabase();
// never store: every key must be absent
fetchMissing(*db, batch_);
}
INSTANTIATE_TEST_SUITE_P(
NodeStoreBackends,
NodeStoreDatabaseTest,
::testing::ValuesIn(allBackends()),
[](::testing::TestParamInfo<std::string> const& info) { return info.param; });
INSTANTIATE_TEST_SUITE_P(
PersistentBackends,
NodeStoreDatabasePersistenceTest,
::testing::ValuesIn(persistentBackends()),
[](::testing::TestParamInfo<std::string> const& info) { return info.param; });
TEST(NodeStoreDatabase, memory_earliest_seq)
{
DummyScheduler scheduler;
beast::TempDir const nodeDb;
Section nodeParams;
nodeParams.set("type", "memory");
nodeParams.set("path", nodeDb.path());
beast::Journal const journal(TestSink::instance());
// default earliest ledger sequence
{
auto db = Manager::instance().makeDatabase(megabytes(4), scheduler, 2, nodeParams, journal);
EXPECT_EQ(db->earliestLedgerSeq(), kXrpLedgerEarliestSeq);
}
// invalid earliest_seq value
{
nodeParams.set("earliest_seq", "0");
try
{
auto db =
Manager::instance().makeDatabase(megabytes(4), scheduler, 2, nodeParams, journal);
FAIL() << "expected runtime_error for earliest_seq=0";
}
catch (std::runtime_error const& e)
{
EXPECT_STREQ(e.what(), "Invalid earliest_seq");
}
}
// valid earliest_seq value
{
nodeParams.set("earliest_seq", "1");
auto db = Manager::instance().makeDatabase(megabytes(4), scheduler, 2, nodeParams, journal);
EXPECT_EQ(db->earliestLedgerSeq(), 1u);
}
}
class DatabaseImportTest : public ::testing::TestWithParam<std::string>
{
};
TEST_P(DatabaseImportTest, same_backend)
{
auto const type = GetParam();
DummyScheduler scheduler;
beast::Journal const journal(TestSink::instance());
beast::TempDir const srcDir;
Section srcParams;
srcParams.set("type", type);
srcParams.set("path", srcDir.path());
auto batch = createPredictableBatch(kNumObjects, kSeedValue);
// write to source db
{
auto src = Manager::instance().makeDatabase(megabytes(4), scheduler, 2, srcParams, journal);
storeBatch(*src, batch);
}
Batch copy;
{
// re-open source and import into a fresh destination
auto src = Manager::instance().makeDatabase(megabytes(4), scheduler, 2, srcParams, journal);
beast::TempDir const destDir;
Section destParams;
destParams.set("type", type);
destParams.set("path", destDir.path());
auto dest =
Manager::instance().makeDatabase(megabytes(4), scheduler, 2, destParams, journal);
dest->importDatabase(*src);
copy = fetchCopyOfBatch(*dest, batch);
}
std::ranges::sort(batch, LessThan{});
std::ranges::sort(copy, LessThan{});
EXPECT_EQ(batch, copy);
}
INSTANTIATE_TEST_SUITE_P(
ImportBackends,
DatabaseImportTest,
::testing::ValuesIn(importBackends()),
[](::testing::TestParamInfo<std::string> const& info) { return info.param; });
} // namespace xrpl::node_store

View File

@@ -0,0 +1,297 @@
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/nodestore/DummyScheduler.h>
#include <xrpl/nodestore/Manager.h>
#include <gtest/gtest.h>
#include <helpers/CaptureSink.h>
#include <helpers/TestSink.h>
#include <nodestore/TestBase.h>
#include <array>
#include <cstddef>
#include <exception>
#include <memory>
#include <string>
#include <utility>
#include <vector>
namespace xrpl::node_store {
namespace {
Section
makeSection(std::string const& path, std::string const& blockSize = "")
{
Section params;
params.set("type", "nudb");
params.set("path", path);
if (!blockSize.empty())
params.set("nudb_block_size", blockSize);
return params;
}
void
runRoundTrip(Section const& params, std::size_t expectedBlocksize)
{
DummyScheduler scheduler;
beast::Journal const journal(TestSink::instance());
auto backend = Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
ASSERT_TRUE(backend);
ASSERT_EQ(backend->getBlockSize(), expectedBlocksize);
backend->open();
ASSERT_TRUE(backend->isOpen());
auto const batch = createPredictableBatch(10, 12345);
storeBatch(*backend, batch);
auto const copy = fetchCopyOfBatch(*backend, batch);
backend->close();
EXPECT_EQ(batch, copy);
}
} // namespace
TEST(NuDBFactory, default_block_size)
{
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path());
ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 4096));
}
TEST(NuDBFactory, valid_block_sizes)
{
auto const kValidSizes = std::to_array<std::size_t>({4096, 8192, 16384, 32768});
for (auto const size : kValidSizes)
{
SCOPED_TRACE("size=" + std::to_string(size));
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), std::to_string(size));
ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, size));
}
// empty value is ignored by config parser; default (4096) is used
{
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), "");
ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 4096));
}
}
TEST(NuDBFactory, invalid_block_sizes)
{
std::vector<std::string> const kInvalidSizes = {
"2048", // too small
"1024", // too small
"65536", // too large
"131072", // too large
"5000", // not power of 2
"6000", // not power of 2
"10000", // not power of 2
"0", // zero
"-1", // negative
"abc", // non-numeric
"4k", // invalid format
"4096.5"}; // decimal
for (auto const& size : kInvalidSizes)
{
SCOPED_TRACE("size='" + size + "'");
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), size);
EXPECT_THROW(runRoundTrip(params, 4096), std::exception);
}
// whitespace handling — lexical_cast may or may not strip; treat as invalid
std::vector<std::string> const kWhitespaceSizes = {"4096 ", " 4096"};
for (auto const& size : kWhitespaceSizes)
{
SCOPED_TRACE("size='" + size + "'");
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), size);
EXPECT_THROW(runRoundTrip(params, 4096), std::exception);
}
}
TEST(NuDBFactory, log_messages)
{
// valid custom block size emits info log
{
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), "8192");
test::CaptureSink sink(beast::Severity::Info);
beast::Journal const journal(sink);
DummyScheduler scheduler;
[[maybe_unused]] auto backend =
Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
EXPECT_TRUE(sink.messages().contains("Using custom NuDB block size: 8192"));
}
// invalid block size throws with informative message
{
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), "5000");
test::CaptureSink sink(beast::Severity::Warning);
beast::Journal const journal(sink);
DummyScheduler scheduler;
try
{
auto backend =
Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
FAIL() << "expected exception for invalid block size 5000";
}
catch (std::exception const& e)
{
std::string const what{e.what()};
EXPECT_TRUE(what.contains("Invalid nudb_block_size: 5000"));
EXPECT_TRUE(what.contains("Must be power of 2 between 4096 and 32768"));
}
}
// non-numeric value throws
{
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), "invalid");
test::CaptureSink sink(beast::Severity::Warning);
beast::Journal const journal(sink);
DummyScheduler scheduler;
try
{
auto backend =
Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
FAIL() << "expected exception for non-numeric block size";
}
catch (std::exception const& e)
{
std::string const what{e.what()};
EXPECT_TRUE(what.contains("Invalid nudb_block_size value: invalid"));
}
}
}
TEST(NuDBFactory, power_of_two_validation)
{
std::vector<std::pair<std::string, bool>> const kCASES = {
{"4095", false}, // just below minimum
{"4096", true}, // minimum valid
{"4097", false}, // not power of 2
{"8192", true}, // valid power of 2
{"8193", false}, // not power of 2
{"16384", true}, // valid power of 2
{"32768", true}, // maximum valid
{"32769", false}, // just above maximum
{"65536", false}}; // power of 2 but too large
for (auto const& [size, shouldWork] : kCASES)
{
SCOPED_TRACE("size=" + size + " shouldWork=" + (shouldWork ? "true" : "false"));
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), size);
test::CaptureSink sink(beast::Severity::Warning);
beast::Journal const journal(sink);
DummyScheduler scheduler;
try
{
auto backend =
Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
EXPECT_TRUE(shouldWork);
}
catch (std::exception const& e)
{
// A throw is only expected for sizes that should NOT work; if a
// valid size throws, fail here instead of silently matching the
// message below (which would mask the regression).
EXPECT_FALSE(shouldWork);
std::string const what{e.what()};
EXPECT_TRUE(what.contains("Invalid nudb_block_size"));
}
}
}
TEST(NuDBFactory, both_constructor_variants)
{
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), "16384");
DummyScheduler scheduler;
beast::Journal const journal(TestSink::instance());
auto backend1 = Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
EXPECT_NE(backend1, nullptr);
ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 16384));
// Test second constructor (with nudb::context)
// Note: This would require access to nudb::context, which might not be
// easily testable without more complex setup. For now, we test that
// the factory can create backends with the first constructor.
}
TEST(NuDBFactory, configuration_parsing)
{
// basic valid format emits success log
{
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), "8192");
test::CaptureSink sink(beast::Severity::Info);
beast::Journal const journal(sink);
DummyScheduler scheduler;
[[maybe_unused]] auto backend =
Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
EXPECT_TRUE(sink.messages().contains("Using custom NuDB block size"));
}
// Test whitespace handling separately since lexical_cast behavior may vary
std::vector<std::string> const kWhitespaceFormats = {" 8192", "8192 "};
for (auto const& format : kWhitespaceFormats)
{
SCOPED_TRACE("format='" + format + "'");
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), format);
test::CaptureSink sink(beast::Severity::Debug);
beast::Journal const journal(sink);
DummyScheduler scheduler;
EXPECT_ANY_THROW(Manager::instance().makeBackend(params, megabytes(4), scheduler, journal));
}
}
TEST(NuDBFactory, data_persistence)
{
std::vector<std::string> const kBlockSizes = {"4096", "8192", "16384", "32768"};
for (auto const& size : kBlockSizes)
{
SCOPED_TRACE("size=" + size);
beast::TempDir const tempDir;
auto const params = makeSection(tempDir.path(), size);
DummyScheduler scheduler;
beast::Journal const journal(TestSink::instance());
// Create test data
auto const batch = createPredictableBatch(50, 54321);
// Store data
{
auto backend =
Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
backend->open();
storeBatch(*backend, batch);
backend->close();
}
// Retrieve data in new backend instance
{
auto backend =
Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
backend->open();
auto const copy = fetchCopyOfBatch(*backend, batch);
EXPECT_EQ(batch, copy);
backend->close();
}
}
}
} // namespace xrpl::node_store

View File

@@ -0,0 +1,169 @@
#pragma once
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/utility/rngfill.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/nodestore/Backend.h>
#include <xrpl/nodestore/Database.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Types.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
namespace xrpl::node_store {
constexpr std::size_t kMinPayloadBytes = 1;
constexpr std::size_t kMaxPayloadBytes = 2000;
constexpr int kNumObjectsToTest = 2000;
constexpr int kNumObjects = 2000;
constexpr std::uint64_t kSeedValue = 50;
struct LessThan
{
bool
operator()(std::shared_ptr<NodeObject> const& lhs, std::shared_ptr<NodeObject> const& rhs)
const noexcept
{
return lhs->getHash() < rhs->getHash();
}
};
[[nodiscard]] inline bool
isSame(std::shared_ptr<NodeObject> const& lhs, std::shared_ptr<NodeObject> const& rhs)
{
return (lhs->getType() == rhs->getType()) && (lhs->getHash() == rhs->getHash()) &&
(lhs->getData() == rhs->getData());
}
[[nodiscard]] inline Batch
createPredictableBatch(std::size_t numObjects, std::uint64_t seed)
{
Batch batch;
batch.reserve(numObjects);
beast::xor_shift_engine rng(seed);
for (auto i = 0uz; i < numObjects; ++i)
{
NodeObjectType const type = [&] {
switch (randInt(rng, 3))
{
case 0:
return NodeObjectType::Ledger;
case 1:
return NodeObjectType::AccountNode;
case 2:
return NodeObjectType::TransactionNode;
case 3:
default:
return NodeObjectType::Unknown;
}
}();
uint256 hash;
beast::rngfill(hash.begin(), hash.size(), rng);
Blob blob(randInt(rng, kMinPayloadBytes, kMaxPayloadBytes));
beast::rngfill(blob.data(), blob.size(), rng);
batch.emplace_back(NodeObject::createObject(type, std::move(blob), hash));
}
return batch;
}
inline void
storeBatch(Backend& backend, Batch const& batch)
{
for (auto const& obj : batch)
backend.store(obj);
}
[[nodiscard]] inline Batch
fetchCopyOfBatch(Backend& backend, Batch const& batch)
{
Batch copy;
copy.reserve(batch.size());
for (auto i = 0uz; i < batch.size(); ++i)
{
SCOPED_TRACE("fetchCopyOfBatch index=" + std::to_string(i));
std::shared_ptr<NodeObject> object;
Status const status = backend.fetch(batch[i]->getHash(), &object);
EXPECT_EQ(status, Status::Ok);
if (status == Status::Ok)
{
EXPECT_NE(object, nullptr);
copy.emplace_back(object);
}
}
return copy;
}
inline void
fetchMissing(Backend& backend, Batch const& batch)
{
for (auto i = 0uz; i < batch.size(); ++i)
{
SCOPED_TRACE("fetchMissing index=" + std::to_string(i));
std::shared_ptr<NodeObject> object;
Status const status = backend.fetch(batch[i]->getHash(), &object);
EXPECT_EQ(status, Status::NotFound);
}
}
inline void
storeBatch(Database& db, Batch const& batch)
{
for (auto const& obj : batch)
{
Blob data(obj->getData());
db.store(obj->getType(), std::move(data), obj->getHash(), db.earliestLedgerSeq());
}
}
[[nodiscard]] inline Batch
fetchCopyOfBatch(Database& db, Batch const& batch)
{
Batch copy;
copy.reserve(batch.size());
for (auto const& obj : batch)
{
std::shared_ptr<NodeObject> const result = db.fetchNodeObject(obj->getHash(), 0);
if (result != nullptr)
copy.emplace_back(result);
}
return copy;
}
inline void
fetchMissing(Database& db, Batch const& batch)
{
for (auto i = 0uz; i < batch.size(); ++i)
{
SCOPED_TRACE("fetchMissing(Database) index=" + std::to_string(i));
EXPECT_EQ(db.fetchNodeObject(batch[i]->getHash(), 0), nullptr);
}
}
} // namespace xrpl::node_store
namespace xrpl {
[[nodiscard]] inline bool
operator==(node_store::Batch const& lhs, node_store::Batch const& rhs)
{
return std::ranges::equal(lhs, rhs, node_store::isSame);
}
} // namespace xrpl

View File

@@ -0,0 +1,46 @@
#include <xrpl/nodestore/detail/varint.h>
#include <gtest/gtest.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
using namespace xrpl::node_store;
TEST(varint, encode_decode)
{
std::vector<std::size_t> const kVALUES = {
0,
1,
2,
126,
127,
128,
253,
254,
255,
16127,
16128,
16129,
0xff,
0xffff,
0xffffffff,
0xffffffffffffUL,
0xffffffffffffffffUL};
for (auto const v : kVALUES)
{
SCOPED_TRACE("value=" + std::to_string(v));
std::array<std::uint8_t, varint_traits<std::size_t>::kMax> vi{};
auto const n0 = writeVarint(vi.data(), v);
EXPECT_GT(n0, 0u) << "write error";
EXPECT_EQ(n0, sizeVarint(v)) << "size error";
std::size_t v1 = 0;
auto const n1 = readVarint(vi.data(), n0, v1);
EXPECT_EQ(n1, n0) << "read error";
EXPECT_EQ(v1, v) << "wrong value";
}
}

View File

@@ -0,0 +1,294 @@
#include <xrpl/peerfinder/detail/Livecache.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/net/IPAddressV4.h>
#include <xrpl/beast/net/IPAddressV6.h>
#include <xrpl/beast/net/IPEndpoint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/PropertyStream.h>
#include <xrpl/json/JsonPropertyStream.h>
#include <xrpl/json/json_forwards.h>
#include <xrpl/json/json_value.h>
#include <xrpl/peerfinder/Types.h>
#include <xrpl/peerfinder/detail/Tuning.h>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <boost/lexical_cast.hpp>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <iterator>
#include <string>
#include <utility>
#include <vector>
namespace xrpl::PeerFinder {
namespace {
class LivecacheTest : public ::testing::Test
{
protected:
static beast::Journal
journal()
{
return beast::Journal{TestSink::instance()};
}
static beast::IP::Endpoint
endpoint(std::uint16_t index, bool v4 = true)
{
auto const port = static_cast<std::uint16_t>(10000 + index);
if (v4)
{
auto bytes = beast::IP::AddressV4::bytes_type{
{54,
static_cast<std::uint8_t>((index / 256) % 256),
static_cast<std::uint8_t>(index % 256),
1}};
return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV4{bytes}}, port};
}
auto bytes = beast::IP::AddressV6::bytes_type{
{0x20,
0x01,
0x0d,
0xb8,
0,
0,
0,
0,
0,
0,
0,
0,
0,
static_cast<std::uint8_t>((index / 256) % 256),
static_cast<std::uint8_t>(index % 256),
1}};
return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV6{bytes}}, port};
}
void
addEndpoint(beast::IP::Endpoint const& ep, std::uint32_t hops = 0)
{
cache_.insert(Endpoint{ep, hops});
}
TestStopwatch clock_;
Livecache<> cache_{clock_, journal()};
};
} // namespace
TEST_F(LivecacheTest, basic_insert)
{
EXPECT_TRUE(cache_.empty());
for (auto i = 0; i < 10; ++i)
addEndpoint(endpoint(i, true));
EXPECT_FALSE(cache_.empty());
EXPECT_EQ(cache_.size(), 10u);
for (auto i = 10; i < 20; ++i)
addEndpoint(endpoint(i, false));
EXPECT_FALSE(cache_.empty());
EXPECT_EQ(cache_.size(), 20u);
}
TEST_F(LivecacheTest, insert_update_keeps_lowest_hop_count)
{
auto const ep1 = Endpoint{endpoint(1), 2};
cache_.insert(ep1);
ASSERT_EQ(cache_.size(), 1u);
EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u);
auto const ep2 = Endpoint{ep1.address, 4};
cache_.insert(ep2);
EXPECT_EQ(cache_.size(), 1u);
EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u);
auto const ep3 = Endpoint{ep1.address, 2};
cache_.insert(ep3);
EXPECT_EQ(cache_.size(), 1u);
EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u);
auto const ep4 = Endpoint{ep1.address, 1};
cache_.insert(ep4);
EXPECT_EQ(cache_.size(), 1u);
EXPECT_EQ((cache_.hops.begin() + 1)->begin()->hops, 1u);
}
TEST_F(LivecacheTest, hop_iterators_support_const_reverse_and_move_back)
{
auto const ep1 = Endpoint{endpoint(1), 1};
auto const ep2 = Endpoint{endpoint(2), 1};
cache_.insert(ep1);
cache_.insert(ep2);
auto hop = *(cache_.hops.begin() + 1);
ASSERT_NE(hop.begin(), hop.end());
ASSERT_NE(hop.cbegin(), hop.cend());
ASSERT_NE(hop.rbegin(), hop.rend());
ASSERT_NE(hop.crbegin(), hop.crend());
auto const firstAddress = hop.begin()->address;
hop.moveBack(hop.begin());
EXPECT_EQ(hop.rbegin()->address, firstAddress);
auto const& constHops = cache_.hops;
EXPECT_NE(constHops.begin(), constHops.end());
EXPECT_NE(constHops.cbegin(), constHops.cend());
EXPECT_NE(constHops.rbegin(), constHops.rend());
EXPECT_NE(constHops.crbegin(), constHops.crend());
auto const constHop = *(constHops.cbegin() + 1);
EXPECT_EQ(std::distance(constHop.begin(), constHop.end()), 2);
EXPECT_EQ(std::distance(constHop.cbegin(), constHop.cend()), 2);
EXPECT_EQ(std::distance(constHop.rbegin(), constHop.rend()), 2);
EXPECT_EQ(std::distance(constHop.crbegin(), constHop.crend()), 2);
}
TEST_F(LivecacheTest, on_write_reports_entries_and_expiration)
{
cache_.insert(Endpoint{endpoint(1), 1});
cache_.insert(Endpoint{endpoint(2), Tuning::kMaxHops + 1});
JsonPropertyStream stream;
{
beast::PropertyStream::Map map(stream);
cache_.onWrite(map);
}
auto const& top = stream.top();
EXPECT_EQ(top["size"].asUInt(), 2u);
EXPECT_FALSE(top["hist"].asString().empty());
ASSERT_TRUE(top.isMember("entries"));
ASSERT_EQ(top["entries"].size(), 2u);
auto const& entry = top["entries"][json::UInt{0}];
EXPECT_TRUE(entry.isMember("hops"));
EXPECT_TRUE(entry.isMember("address"));
EXPECT_TRUE(entry.isMember("expires"));
}
TEST_F(LivecacheTest, expire_removes_entries_after_ttl)
{
using namespace std::chrono_literals;
cache_.insert(Endpoint{endpoint(1), 1});
ASSERT_EQ(cache_.size(), 1u);
cache_.expire();
EXPECT_EQ(cache_.size(), 1u);
clock_.advance(Tuning::kLiveCacheSecondsToLive - 1s);
cache_.expire();
EXPECT_EQ(cache_.size(), 1u);
clock_.advance(1s);
cache_.expire();
EXPECT_TRUE(cache_.empty());
}
TEST_F(LivecacheTest, expire_removes_multiple_entries_after_ttl)
{
using namespace std::chrono_literals;
cache_.insert(Endpoint{endpoint(1), 1});
cache_.insert(Endpoint{endpoint(2), 2});
clock_.advance(Tuning::kLiveCacheSecondsToLive);
cache_.expire();
EXPECT_TRUE(cache_.empty());
}
TEST_F(LivecacheTest, histogram_counts_all_entries)
{
constexpr auto kNumEndpoints = 40;
for (auto i = 0; i < kNumEndpoints; ++i)
{
addEndpoint(endpoint(static_cast<std::uint16_t>(i)), xrpl::randInt<std::uint32_t>());
}
auto const histogram = cache_.hops.histogram();
ASSERT_FALSE(histogram.empty());
std::vector<std::string> values;
boost::split(values, histogram, boost::algorithm::is_any_of(","));
auto sum = 0;
for (auto const& value : values)
{
auto const count = boost::lexical_cast<int>(boost::trim_copy(value));
sum += count;
EXPECT_GE(count, 0);
}
EXPECT_EQ(sum, kNumEndpoints);
}
TEST_F(LivecacheTest, shuffle_preserves_bucket_contents)
{
for (auto i = 0; i < 100; ++i)
{
addEndpoint(endpoint(static_cast<std::uint16_t>(i)), xrpl::randInt(Tuning::kMaxHops + 1));
}
using AtHop = std::vector<Endpoint>;
using AllHops = std::array<AtHop, 1 + Tuning::kMaxHops + 1>;
auto const compareEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) {
return rhs.hops < lhs.hops || (rhs.hops == lhs.hops && rhs.address < lhs.address);
};
auto const sameEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) {
return lhs.hops == rhs.hops && lhs.address == rhs.address;
};
auto const sameEndpoints =
[&sameEndpoint](std::vector<Endpoint> const& lhs, std::vector<Endpoint> const& rhs) {
return lhs.size() == rhs.size() &&
std::equal(lhs.begin(), lhs.end(), rhs.begin(), sameEndpoint);
};
AllHops before;
AllHops beforeSorted;
for (auto i = std::make_pair(0, cache_.hops.begin()); i.second != cache_.hops.end();
++i.first, ++i.second)
{
std::ranges::copy(*i.second, std::back_inserter(before[i.first]));
std::ranges::copy(*i.second, std::back_inserter(beforeSorted[i.first]));
std::ranges::sort(beforeSorted[i.first], compareEndpoint);
}
cache_.hops.shuffle();
AllHops after;
AllHops afterSorted;
for (auto i = std::make_pair(0, cache_.hops.begin()); i.second != cache_.hops.end();
++i.first, ++i.second)
{
std::ranges::copy(*i.second, std::back_inserter(after[i.first]));
std::ranges::copy(*i.second, std::back_inserter(afterSorted[i.first]));
std::ranges::sort(afterSorted[i.first], compareEndpoint);
}
auto allBucketsKeptOriginalOrder = true;
for (auto i = 0u; i < before.size(); ++i)
{
EXPECT_EQ(before[i].size(), after[i].size());
allBucketsKeptOriginalOrder =
allBucketsKeptOriginalOrder && sameEndpoints(before[i], after[i]);
EXPECT_TRUE(sameEndpoints(beforeSorted[i], afterSorted[i]));
}
EXPECT_FALSE(allBucketsKeptOriginalOrder);
}
} // namespace xrpl::PeerFinder

File diff suppressed because it is too large Load Diff

View File

@@ -24,13 +24,13 @@ namespace xrpl::tests {
class TestNodeFamily : public Family
{
private:
std::unique_ptr<NodeStore::Database> db_;
std::unique_ptr<node_store::Database> db_;
std::shared_ptr<FullBelowCache> fbCache_;
std::shared_ptr<TreeNodeCache> tnCache_;
TestStopwatch clock_;
NodeStore::DummyScheduler scheduler_;
node_store::DummyScheduler scheduler_;
beast::Journal const j_;
@@ -49,17 +49,17 @@ public:
Section testSection;
testSection.set(Keys::kType, "memory");
testSection.set(Keys::kPath, "SHAMap_test");
db_ = NodeStore::Manager::instance().makeDatabase(
db_ = node_store::Manager::instance().makeDatabase(
megabytes(4), scheduler_, 1, testSection, j);
}
NodeStore::Database&
node_store::Database&
db() override
{
return *db_;
}
[[nodiscard]] NodeStore::Database const&
[[nodiscard]] node_store::Database const&
db() const override
{
return *db_;