mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-31 11:00:23 +00:00
Merge branch 'pratik/otel-phase2-rpc-tracing' into pratik/otel-phase3-tx-tracing
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
81
src/tests/libxrpl/consensus/ByzantineFailureSim.cpp
Normal file
81
src/tests/libxrpl/consensus/ByzantineFailureSim.cpp
Normal 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
|
||||
81
src/tests/libxrpl/consensus/CensorshipDetector.cpp
Normal file
81
src/tests/libxrpl/consensus/CensorshipDetector.cpp
Normal 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
|
||||
1455
src/tests/libxrpl/consensus/Consensus.cpp
Normal file
1455
src/tests/libxrpl/consensus/Consensus.cpp
Normal file
File diff suppressed because it is too large
Load Diff
252
src/tests/libxrpl/consensus/DistributedValidatorsSim.cpp
Normal file
252
src/tests/libxrpl/consensus/DistributedValidatorsSim.cpp
Normal 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
|
||||
105
src/tests/libxrpl/consensus/LedgerTiming.cpp
Normal file
105
src/tests/libxrpl/consensus/LedgerTiming.cpp
Normal 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
|
||||
693
src/tests/libxrpl/consensus/LedgerTrie.cpp
Normal file
693
src/tests/libxrpl/consensus/LedgerTrie.cpp
Normal 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
|
||||
100
src/tests/libxrpl/consensus/ScaleFreeSim.cpp
Normal file
100
src/tests/libxrpl/consensus/ScaleFreeSim.cpp
Normal 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
|
||||
1030
src/tests/libxrpl/consensus/Validations.cpp
Normal file
1030
src/tests/libxrpl/consensus/Validations.cpp
Normal file
File diff suppressed because it is too large
Load Diff
122
src/tests/libxrpl/csf/BasicNetwork.cpp
Normal file
122
src/tests/libxrpl/csf/BasicNetwork.cpp
Normal 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
|
||||
232
src/tests/libxrpl/csf/BasicNetwork.h
Normal file
232
src/tests/libxrpl/csf/BasicNetwork.h
Normal 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
|
||||
333
src/tests/libxrpl/csf/CollectorRef.h
Normal file
333
src/tests/libxrpl/csf/CollectorRef.h
Normal 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
|
||||
72
src/tests/libxrpl/csf/Digraph.cpp
Normal file
72
src/tests/libxrpl/csf/Digraph.cpp
Normal 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
|
||||
238
src/tests/libxrpl/csf/Digraph.h
Normal file
238
src/tests/libxrpl/csf/Digraph.h
Normal 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
|
||||
59
src/tests/libxrpl/csf/Histogram.cpp
Normal file
59
src/tests/libxrpl/csf/Histogram.cpp
Normal 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
|
||||
121
src/tests/libxrpl/csf/Histogram.h
Normal file
121
src/tests/libxrpl/csf/Histogram.h
Normal 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
|
||||
1023
src/tests/libxrpl/csf/Peer.h
Normal file
1023
src/tests/libxrpl/csf/Peer.h
Normal file
File diff suppressed because it is too large
Load Diff
351
src/tests/libxrpl/csf/PeerGroup.h
Normal file
351
src/tests/libxrpl/csf/PeerGroup.h
Normal 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
|
||||
16
src/tests/libxrpl/csf/Proposal.h
Normal file
16
src/tests/libxrpl/csf/Proposal.h
Normal 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
|
||||
192
src/tests/libxrpl/csf/README.md
Normal file
192
src/tests/libxrpl/csf/README.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
61
src/tests/libxrpl/csf/Scheduler.cpp
Normal file
61
src/tests/libxrpl/csf/Scheduler.cpp
Normal 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
|
||||
443
src/tests/libxrpl/csf/Scheduler.h
Normal file
443
src/tests/libxrpl/csf/Scheduler.h
Normal 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
171
src/tests/libxrpl/csf/Sim.h
Normal 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
|
||||
17
src/tests/libxrpl/csf/SimTime.h
Normal file
17
src/tests/libxrpl/csf/SimTime.h
Normal 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
|
||||
152
src/tests/libxrpl/csf/TrustGraph.h
Normal file
152
src/tests/libxrpl/csf/TrustGraph.h
Normal 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
228
src/tests/libxrpl/csf/Tx.h
Normal 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
|
||||
177
src/tests/libxrpl/csf/Validation.h
Normal file
177
src/tests/libxrpl/csf/Validation.h
Normal 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
|
||||
671
src/tests/libxrpl/csf/collectors.h
Normal file
671
src/tests/libxrpl/csf/collectors.h
Normal 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
|
||||
BIN
src/tests/libxrpl/csf/csf_graph.png
Normal file
BIN
src/tests/libxrpl/csf/csf_graph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
BIN
src/tests/libxrpl/csf/csf_overview.png
Normal file
BIN
src/tests/libxrpl/csf/csf_overview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
157
src/tests/libxrpl/csf/events.h
Normal file
157
src/tests/libxrpl/csf/events.h
Normal 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
|
||||
70
src/tests/libxrpl/csf/impl/Sim.cpp
Normal file
70
src/tests/libxrpl/csf/impl/Sim.cpp
Normal 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
|
||||
167
src/tests/libxrpl/csf/impl/ledgers.cpp
Normal file
167
src/tests/libxrpl/csf/impl/ledgers.cpp
Normal 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
|
||||
360
src/tests/libxrpl/csf/ledgers.h
Normal file
360
src/tests/libxrpl/csf/ledgers.h
Normal 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
|
||||
158
src/tests/libxrpl/csf/random.h
Normal file
158
src/tests/libxrpl/csf/random.h
Normal 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
|
||||
111
src/tests/libxrpl/csf/submitters.h
Normal file
111
src/tests/libxrpl/csf/submitters.h
Normal 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
|
||||
63
src/tests/libxrpl/csf/timers.h
Normal file
63
src/tests/libxrpl/csf/timers.h
Normal 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
|
||||
50
src/tests/libxrpl/helpers/CaptureSink.h
Normal file
50
src/tests/libxrpl/helpers/CaptureSink.h
Normal 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
|
||||
@@ -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_;
|
||||
|
||||
@@ -221,7 +221,7 @@ public:
|
||||
}
|
||||
|
||||
// Storage services
|
||||
NodeStore::Database&
|
||||
node_store::Database&
|
||||
getNodeStore() override
|
||||
{
|
||||
throw std::logic_error("TestServiceRegistry::getNodeStore() not implemented");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
182
src/tests/libxrpl/nodestore/Backend.cpp
Normal file
182
src/tests/libxrpl/nodestore/Backend.cpp
Normal 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
|
||||
41
src/tests/libxrpl/nodestore/Basics.cpp
Normal file
41
src/tests/libxrpl/nodestore/Basics.cpp
Normal 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
|
||||
248
src/tests/libxrpl/nodestore/Database.cpp
Normal file
248
src/tests/libxrpl/nodestore/Database.cpp
Normal 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
|
||||
297
src/tests/libxrpl/nodestore/NuDBFactory.cpp
Normal file
297
src/tests/libxrpl/nodestore/NuDBFactory.cpp
Normal 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
|
||||
169
src/tests/libxrpl/nodestore/TestBase.h
Normal file
169
src/tests/libxrpl/nodestore/TestBase.h
Normal 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
|
||||
46
src/tests/libxrpl/nodestore/varint.cpp
Normal file
46
src/tests/libxrpl/nodestore/varint.cpp
Normal 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";
|
||||
}
|
||||
}
|
||||
294
src/tests/libxrpl/peerfinder/Livecache.cpp
Normal file
294
src/tests/libxrpl/peerfinder/Livecache.cpp
Normal 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
|
||||
1270
src/tests/libxrpl/peerfinder/PeerFinder.cpp
Normal file
1270
src/tests/libxrpl/peerfinder/PeerFinder.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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_;
|
||||
|
||||
Reference in New Issue
Block a user