mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-22 14:50:54 +00:00
Merge develop
This commit is contained in:
@@ -498,7 +498,7 @@ class Batch_test : public beast::unit_test::Suite
|
||||
auto const batchFee = batch::calcBatchFee(env, 0, 2);
|
||||
auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
|
||||
tx1[jss::Fee] = "1.5";
|
||||
env.setParseFailureExpected(true);
|
||||
auto const g = env.getParseFailureGuard(true);
|
||||
try
|
||||
{
|
||||
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
|
||||
@@ -510,7 +510,6 @@ class Batch_test : public beast::unit_test::Suite
|
||||
{
|
||||
BEAST_EXPECT(true);
|
||||
}
|
||||
env.setParseFailureExpected(false);
|
||||
}
|
||||
|
||||
// temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence
|
||||
|
||||
@@ -2001,6 +2001,61 @@ class PermissionedDEX_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testReplaceDomainOfferWithOtherDomainOffer(FeatureBitset features)
|
||||
{
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
|
||||
testcase << "Replace domain offer via OfferCreate"
|
||||
<< (fixEnabled ? " (fixCleanup3_4_0 enabled)" : " (fixCleanup3_4_0 disabled)");
|
||||
|
||||
Env env(*this, features);
|
||||
auto const& [gw, domainOwner, alice, bob, carol, USD, domainA, credType] =
|
||||
PermissionedDEX(env);
|
||||
|
||||
Account const domainOwnerB("permdex-domainOwnerB");
|
||||
auto const domainB =
|
||||
setupDomain(env, {alice, bob, carol, gw}, domainOwnerB, "permdex-other-domain");
|
||||
BEAST_EXPECT(domainA != domainB);
|
||||
|
||||
auto const oldSeq = env.seq(alice);
|
||||
env(offer(alice, USD(100), XRP(1)), Domain(domainA));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(checkOffer(env, alice, oldSeq, USD(100), XRP(1), 0, true));
|
||||
auto const oldOffer = env.le(keylet::offer(alice.id(), oldSeq));
|
||||
if (!BEAST_EXPECT(oldOffer))
|
||||
return;
|
||||
BEAST_EXPECT(oldOffer->getFieldH256(sfDomainID) == domainA);
|
||||
|
||||
auto const newSeq = env.seq(alice);
|
||||
// The invariant should reject mixing active Permissioned DEX domains,
|
||||
// not a domain that is only touched because its offer is being deleted.
|
||||
if (fixEnabled)
|
||||
{
|
||||
env(offer(alice, USD(100), XRP(2)), Domain(domainB), Json(jss::OfferSequence, oldSeq));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!offerExists(env, alice, oldSeq));
|
||||
BEAST_EXPECT(checkOffer(env, alice, newSeq, USD(100), XRP(2), 0, true));
|
||||
auto const newOffer = env.le(keylet::offer(alice.id(), newSeq));
|
||||
if (!BEAST_EXPECT(newOffer))
|
||||
return;
|
||||
BEAST_EXPECT(newOffer->getFieldH256(sfDomainID) == domainB);
|
||||
}
|
||||
else
|
||||
{
|
||||
env(offer(alice, USD(100), XRP(2)),
|
||||
Domain(domainB),
|
||||
Json(jss::OfferSequence, oldSeq),
|
||||
Ter(tecINVARIANT_FAILED));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(checkOffer(env, alice, oldSeq, USD(100), XRP(1), 0, true));
|
||||
BEAST_EXPECT(!offerExists(env, alice, newSeq));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -2038,6 +2093,8 @@ public:
|
||||
// only after fixCleanup3_2_0.
|
||||
testCancelRegularOfferWithDomainCreate(all);
|
||||
testCancelRegularOfferWithDomainCreate(all - fixCleanup3_2_0);
|
||||
testReplaceDomainOfferWithOtherDomainOffer(all);
|
||||
testReplaceDomainOfferWithOtherDomainOffer(all - fixCleanup3_4_0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpld/app/consensus/RCLValidations.h>
|
||||
#include <xrpld/consensus/LedgerTrie.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/consensus/LedgerTrie.h>
|
||||
#include <xrpl/ledger/Ledger.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
|
||||
@@ -5524,9 +5524,9 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
|
||||
// 2. Mantissa larger than uint64 max
|
||||
env.setParseFailureExpected(true);
|
||||
try
|
||||
{
|
||||
auto const g = env.getParseFailureGuard(true);
|
||||
tx[sfAssetsMaximum] = "18446744073709551617e5"; // uint64 max + 1
|
||||
env(tx);
|
||||
BEAST_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max");
|
||||
@@ -5537,7 +5537,6 @@ class Vault_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(
|
||||
e.what() == "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."s);
|
||||
}
|
||||
env.setParseFailureExpected(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
#include <test/csf/Peer.h>
|
||||
#include <test/csf/PeerGroup.h>
|
||||
#include <test/csf/Sim.h>
|
||||
#include <test/csf/SimTime.h>
|
||||
#include <test/csf/TrustGraph.h>
|
||||
#include <test/csf/collectors.h>
|
||||
|
||||
#include <xrpld/consensus/ConsensusParms.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <ios>
|
||||
#include <iostream>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class ByzantineFailureSim_test : public beast::unit_test::Suite
|
||||
{
|
||||
void
|
||||
run() override
|
||||
{
|
||||
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.
|
||||
pass();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE_MANUAL(ByzantineFailureSim, consensus, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,253 +0,0 @@
|
||||
#include <test/csf/PeerGroup.h>
|
||||
#include <test/csf/Sim.h>
|
||||
#include <test/csf/collectors.h>
|
||||
#include <test/csf/random.h>
|
||||
#include <test/csf/submitters.h>
|
||||
#include <test/csf/timers.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
#include <ostream>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* In progress simulations for diversifying and distributing validators
|
||||
*/
|
||||
class DistributedValidators_test : public beast::unit_test::Suite
|
||||
{
|
||||
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
|
||||
log << prefix << "(" << numPeers << "," << delay.count() << ")" << std::endl;
|
||||
|
||||
// number of peers, UNLs, connections
|
||||
BEAST_EXPECT(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);
|
||||
|
||||
// BEAST_EXPECT(sim.branches() == 1);
|
||||
// BEAST_EXPECT(sim.synchronized());
|
||||
|
||||
log << std::right;
|
||||
log << "| Peers: " << std::setw(2) << peers.size();
|
||||
log << " | Duration: " << std::setw(6) << duration_cast<milliseconds>(simDuration).count()
|
||||
<< " ms";
|
||||
log << " | Branches: " << std::setw(1) << sim.branches();
|
||||
log << " | Synchronized: " << std::setw(1) << (sim.synchronized() ? "Y" : "N");
|
||||
log << " |" << std::endl;
|
||||
|
||||
txCollector.report(simDuration, log, true);
|
||||
ledgerCollector.report(simDuration, log, false);
|
||||
|
||||
std::string const tag = std::to_string(numPeers);
|
||||
txCollector.csv(simDuration, txLog, tag, printHeaders);
|
||||
ledgerCollector.csv(simDuration, ledgerLog, tag, printHeaders);
|
||||
|
||||
log << 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
|
||||
log << 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);
|
||||
BEAST_EXPECT(numPeers >= 1);
|
||||
BEAST_EXPECT(numCNLs >= 1);
|
||||
BEAST_EXPECT(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);
|
||||
|
||||
// BEAST_EXPECT(sim.branches() == 1);
|
||||
// BEAST_EXPECT(sim.synchronized());
|
||||
|
||||
log << std::right;
|
||||
log << "| Peers: " << std::setw(2) << peers.size();
|
||||
log << " | Duration: " << std::setw(6) << duration_cast<milliseconds>(simDuration).count()
|
||||
<< " ms";
|
||||
log << " | Branches: " << std::setw(1) << sim.branches();
|
||||
log << " | Synchronized: " << std::setw(1) << (sim.synchronized() ? "Y" : "N");
|
||||
log << " |" << std::endl;
|
||||
|
||||
txCollector.report(simDuration, log, true);
|
||||
ledgerCollector.report(simDuration, log, false);
|
||||
|
||||
std::string const tag = std::to_string(numPeers);
|
||||
txCollector.csv(simDuration, txLog, tag, printHeaders);
|
||||
ledgerCollector.csv(simDuration, ledgerLog, tag, printHeaders);
|
||||
|
||||
log << std::endl;
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
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);
|
||||
|
||||
log << "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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(DistributedValidators, consensus, xrpl, 2);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1,118 +0,0 @@
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/ledger/LedgerTiming.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class LedgerTiming_test : public beast::unit_test::Suite
|
||||
{
|
||||
void
|
||||
testGetNextLedgerTimeResolution()
|
||||
{
|
||||
// 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);
|
||||
BEAST_EXPECT(decreases.increase == 3);
|
||||
BEAST_EXPECT(decreases.decrease == 0);
|
||||
BEAST_EXPECT(decreases.equal == 7);
|
||||
|
||||
// If we always agree on close time, only can decrease resolution
|
||||
// until hit the min
|
||||
auto increases = TestRes::run(false, 100);
|
||||
BEAST_EXPECT(increases.increase == 3);
|
||||
BEAST_EXPECT(increases.decrease == 0);
|
||||
BEAST_EXPECT(increases.equal == 97);
|
||||
}
|
||||
|
||||
void
|
||||
testRoundCloseTime()
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
// A closeTime equal to the epoch is not modified
|
||||
using tp = NetClock::time_point;
|
||||
tp const def;
|
||||
BEAST_EXPECT(def == roundCloseTime(def, 30s));
|
||||
|
||||
// Otherwise, the closeTime is rounded to the nearest
|
||||
// rounding up on ties
|
||||
BEAST_EXPECT(tp{0s} == roundCloseTime(tp{29s}, 60s));
|
||||
BEAST_EXPECT(tp{30s} == roundCloseTime(tp{30s}, 1s));
|
||||
BEAST_EXPECT(tp{60s} == roundCloseTime(tp{31s}, 60s));
|
||||
BEAST_EXPECT(tp{60s} == roundCloseTime(tp{30s}, 60s));
|
||||
BEAST_EXPECT(tp{60s} == roundCloseTime(tp{59s}, 60s));
|
||||
BEAST_EXPECT(tp{60s} == roundCloseTime(tp{60s}, 60s));
|
||||
BEAST_EXPECT(tp{60s} == roundCloseTime(tp{61s}, 60s));
|
||||
}
|
||||
|
||||
void
|
||||
testEffCloseTime()
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
using tp = NetClock::time_point;
|
||||
tp close = effCloseTime(tp{10s}, 30s, tp{0s});
|
||||
BEAST_EXPECT(close == tp{1s});
|
||||
|
||||
close = effCloseTime(tp{16s}, 30s, tp{0s});
|
||||
BEAST_EXPECT(close == tp{30s});
|
||||
|
||||
close = effCloseTime(tp{16s}, 30s, tp{30s});
|
||||
BEAST_EXPECT(close == tp{31s});
|
||||
|
||||
close = effCloseTime(tp{16s}, 30s, tp{60s});
|
||||
BEAST_EXPECT(close == tp{61s});
|
||||
|
||||
close = effCloseTime(tp{31s}, 30s, tp{0s});
|
||||
BEAST_EXPECT(close == tp{30s});
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testGetNextLedgerTimeResolution();
|
||||
testRoundCloseTime();
|
||||
testEffCloseTime();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(LedgerTiming, consensus, xrpl);
|
||||
} // namespace xrpl::test
|
||||
@@ -1,716 +0,0 @@
|
||||
#include <test/csf/ledgers.h>
|
||||
|
||||
#include <xrpld/consensus/LedgerTrie.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class LedgerTrie_test : public beast::unit_test::Suite
|
||||
{
|
||||
void
|
||||
testInsert()
|
||||
{
|
||||
using namespace csf;
|
||||
// Single entry by itself
|
||||
{
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
t.insert(h["abc"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 1);
|
||||
|
||||
t.insert(h["abc"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 2);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 2);
|
||||
}
|
||||
// Suffix of existing (extending tree)
|
||||
{
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
t.insert(h["abc"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
// extend with no siblings
|
||||
t.insert(h["abcd"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 2);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1);
|
||||
|
||||
// extend with existing sibling
|
||||
t.insert(h["abce"]);
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 3);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h["abce"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abce"]) == 1);
|
||||
}
|
||||
// uncommitted of existing node
|
||||
{
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
t.insert(h["abcd"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
// uncommitted with no siblings
|
||||
t.insert(h["abcdf"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcd"]) == 2);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcdf"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcdf"]) == 1);
|
||||
|
||||
// uncommitted with existing child
|
||||
t.insert(h["abc"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 3);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcd"]) == 2);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcdf"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcdf"]) == 1);
|
||||
}
|
||||
// Suffix + uncommitted of existing node
|
||||
{
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
t.insert(h["abcd"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
t.insert(h["abce"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 0);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 2);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h["abce"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abce"]) == 1);
|
||||
}
|
||||
// Suffix + uncommitted with existing child
|
||||
{
|
||||
// abcd : abcde, abcf
|
||||
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
t.insert(h["abcd"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
t.insert(h["abcde"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
t.insert(h["abcf"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 0);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 3);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcd"]) == 2);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcf"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcf"]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcde"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcde"]) == 1);
|
||||
}
|
||||
|
||||
// Multiple counts
|
||||
{
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
t.insert(h["ab"], 4);
|
||||
BEAST_EXPECT(t.tipSupport(h["ab"]) == 4);
|
||||
BEAST_EXPECT(t.branchSupport(h["ab"]) == 4);
|
||||
BEAST_EXPECT(t.tipSupport(h["a"]) == 0);
|
||||
BEAST_EXPECT(t.branchSupport(h["a"]) == 4);
|
||||
|
||||
t.insert(h["abc"], 2);
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 2);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 2);
|
||||
BEAST_EXPECT(t.tipSupport(h["ab"]) == 4);
|
||||
BEAST_EXPECT(t.branchSupport(h["ab"]) == 6);
|
||||
BEAST_EXPECT(t.tipSupport(h["a"]) == 0);
|
||||
BEAST_EXPECT(t.branchSupport(h["a"]) == 6);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRemove()
|
||||
{
|
||||
using namespace csf;
|
||||
// Not in trie
|
||||
{
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
t.insert(h["abc"]);
|
||||
|
||||
BEAST_EXPECT(!t.remove(h["ab"]));
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(!t.remove(h["a"]));
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
}
|
||||
// In trie but with 0 tip support
|
||||
{
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
t.insert(h["abcd"]);
|
||||
t.insert(h["abce"]);
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 0);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 2);
|
||||
BEAST_EXPECT(!t.remove(h["abc"]));
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 0);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 2);
|
||||
}
|
||||
// In trie with > 1 tip support
|
||||
{
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
t.insert(h["abc"], 2);
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 2);
|
||||
BEAST_EXPECT(t.remove(h["abc"]));
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
|
||||
t.insert(h["abc"], 1);
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 2);
|
||||
BEAST_EXPECT(t.remove(h["abc"], 2));
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 0);
|
||||
|
||||
t.insert(h["abc"], 3);
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 3);
|
||||
BEAST_EXPECT(t.remove(h["abc"], 300));
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(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"]);
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["ab"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["ab"]) == 2);
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 1);
|
||||
|
||||
BEAST_EXPECT(t.remove(h["abc"]));
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.tipSupport(h["ab"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["ab"]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 0);
|
||||
BEAST_EXPECT(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"]);
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 2);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1);
|
||||
|
||||
BEAST_EXPECT(t.remove(h["abc"]));
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 0);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1);
|
||||
BEAST_EXPECT(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"]);
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 3);
|
||||
|
||||
BEAST_EXPECT(t.remove(h["abc"]));
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 0);
|
||||
BEAST_EXPECT(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"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
t.remove(h["ab"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h["abd"]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h["ab"]) == 0);
|
||||
BEAST_EXPECT(t.branchSupport(h["ab"]) == 2);
|
||||
|
||||
t.remove(h["abd"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["ab"]) == 1);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testEmpty()
|
||||
{
|
||||
using namespace csf;
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
BEAST_EXPECT(t.empty());
|
||||
|
||||
Ledger const genesis = h[""];
|
||||
t.insert(genesis);
|
||||
BEAST_EXPECT(!t.empty());
|
||||
t.remove(genesis);
|
||||
BEAST_EXPECT(t.empty());
|
||||
|
||||
t.insert(h["abc"]);
|
||||
BEAST_EXPECT(!t.empty());
|
||||
t.remove(h["abc"]);
|
||||
BEAST_EXPECT(t.empty());
|
||||
}
|
||||
|
||||
void
|
||||
testSupport()
|
||||
{
|
||||
using namespace csf;
|
||||
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
BEAST_EXPECT(t.tipSupport(h["a"]) == 0);
|
||||
BEAST_EXPECT(t.tipSupport(h["axy"]) == 0);
|
||||
|
||||
BEAST_EXPECT(t.branchSupport(h["a"]) == 0);
|
||||
BEAST_EXPECT(t.branchSupport(h["axy"]) == 0);
|
||||
|
||||
t.insert(h["abc"]);
|
||||
BEAST_EXPECT(t.tipSupport(h["a"]) == 0);
|
||||
BEAST_EXPECT(t.tipSupport(h["ab"]) == 0);
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h["abcd"]) == 0);
|
||||
|
||||
BEAST_EXPECT(t.branchSupport(h["a"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["ab"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abcd"]) == 0);
|
||||
|
||||
t.insert(h["abe"]);
|
||||
BEAST_EXPECT(t.tipSupport(h["a"]) == 0);
|
||||
BEAST_EXPECT(t.tipSupport(h["ab"]) == 0);
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h["abe"]) == 1);
|
||||
|
||||
BEAST_EXPECT(t.branchSupport(h["a"]) == 2);
|
||||
BEAST_EXPECT(t.branchSupport(h["ab"]) == 2);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abe"]) == 1);
|
||||
|
||||
t.remove(h["abc"]);
|
||||
BEAST_EXPECT(t.tipSupport(h["a"]) == 0);
|
||||
BEAST_EXPECT(t.tipSupport(h["ab"]) == 0);
|
||||
BEAST_EXPECT(t.tipSupport(h["abc"]) == 0);
|
||||
BEAST_EXPECT(t.tipSupport(h["abe"]) == 1);
|
||||
|
||||
BEAST_EXPECT(t.branchSupport(h["a"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["ab"]) == 1);
|
||||
BEAST_EXPECT(t.branchSupport(h["abc"]) == 0);
|
||||
BEAST_EXPECT(t.branchSupport(h["abe"]) == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testGetPreferred()
|
||||
{
|
||||
using namespace csf;
|
||||
using Seq = Ledger::Seq;
|
||||
// Empty
|
||||
{
|
||||
LedgerTrie<Ledger> const t;
|
||||
BEAST_EXPECT(t.getPreferred(Seq{0}) == std::nullopt);
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{0})->id == genesis.id());
|
||||
BEAST_EXPECT(t.remove(genesis));
|
||||
BEAST_EXPECT(t.getPreferred(Seq{0}) == std::nullopt);
|
||||
BEAST_EXPECT(!t.remove(genesis));
|
||||
}
|
||||
// Single node no children
|
||||
{
|
||||
LedgerTrie<Ledger> t;
|
||||
LedgerHistoryHelper h;
|
||||
t.insert(h["abc"]);
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id());
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abcd"].id());
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abc"].id());
|
||||
|
||||
t.insert(h["abc"]);
|
||||
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id());
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abc"].id());
|
||||
|
||||
t.insert(h["abcd"]);
|
||||
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abcd"].id());
|
||||
BEAST_EXPECT(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);
|
||||
|
||||
BEAST_EXPECT(h["abce"].id() > h["abcd"].id());
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abce"].id());
|
||||
|
||||
t.insert(h["abcd"]);
|
||||
BEAST_EXPECT(h["abce"].id() > h["abcd"].id());
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
BEAST_EXPECT(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
|
||||
BEAST_EXPECT(h["abce"].id() > h["abcd"].id());
|
||||
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abce"].id());
|
||||
BEAST_EXPECT(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"]);
|
||||
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id());
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abcde"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abcde"].id());
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abc"].id());
|
||||
BEAST_EXPECT(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
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abcde"].id());
|
||||
BEAST_EXPECT(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
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{1})->id == h["ab"].id());
|
||||
BEAST_EXPECT(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
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["a"].id());
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{1})->id == h["ab"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{2})->id == h["ab"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["a"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["a"].id());
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{1})->id == h["ab"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{2})->id == h["ab"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["ab"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["a"].id());
|
||||
BEAST_EXPECT(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)
|
||||
BEAST_EXPECT(t.getPreferred(Seq{1})->id == h["abde"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{2})->id == h["abde"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abde"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["ab"].id());
|
||||
BEAST_EXPECT(t.getPreferred(Seq{5})->id == h["ab"].id());
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRootRelated()
|
||||
{
|
||||
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;
|
||||
BEAST_EXPECT(!t.remove(h[""]));
|
||||
BEAST_EXPECT(t.branchSupport(h[""]) == 0);
|
||||
BEAST_EXPECT(t.tipSupport(h[""]) == 0);
|
||||
|
||||
t.insert(h["a"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.branchSupport(h[""]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h[""]) == 0);
|
||||
|
||||
t.insert(h["e"]);
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.branchSupport(h[""]) == 2);
|
||||
BEAST_EXPECT(t.tipSupport(h[""]) == 0);
|
||||
|
||||
BEAST_EXPECT(t.remove(h["e"]));
|
||||
BEAST_EXPECT(t.checkInvariants());
|
||||
BEAST_EXPECT(t.branchSupport(h[""]) == 1);
|
||||
BEAST_EXPECT(t.tipSupport(h[""]) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testStress()
|
||||
{
|
||||
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]);
|
||||
}
|
||||
if (!BEAST_EXPECT(t.checkInvariants()))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testInsert();
|
||||
testRemove();
|
||||
testEmpty();
|
||||
testSupport();
|
||||
testGetPreferred();
|
||||
testRootRelated();
|
||||
testStress();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(LedgerTrie, consensus, xrpl);
|
||||
} // namespace xrpl::test
|
||||
@@ -1,83 +0,0 @@
|
||||
#include <xrpld/app/consensus/RCLCensorshipDetector.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class RCLCensorshipDetector_test : public beast::unit_test::Suite
|
||||
{
|
||||
void
|
||||
test(
|
||||
RCLCensorshipDetector<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
|
||||
RCLCensorshipDetector<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:
|
||||
BEAST_EXPECT(remain.empty());
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testcase("Censorship Detector");
|
||||
|
||||
RCLCensorshipDetector<int, int> cdet;
|
||||
int round = 0;
|
||||
// proposed accepted remain remove
|
||||
test(cdet, ++round, {}, {}, {}, {});
|
||||
test(cdet, ++round, {10, 11, 12, 13}, {11, 2}, {10, 13}, {});
|
||||
test(cdet, ++round, {10, 13, 14, 15}, {14}, {10, 13, 15}, {});
|
||||
test(cdet, ++round, {10, 13, 15, 16}, {15, 16}, {10, 13}, {});
|
||||
test(cdet, ++round, {10, 13}, {17, 18}, {10, 13}, {});
|
||||
test(cdet, ++round, {10, 19}, {}, {10, 19}, {});
|
||||
test(cdet, ++round, {10, 19, 20}, {20}, {10}, {19});
|
||||
test(cdet, ++round, {21}, {21}, {}, {});
|
||||
test(cdet, ++round, {}, {22}, {}, {});
|
||||
test(cdet, ++round, {23, 24, 25, 26}, {25, 27}, {23, 26}, {24});
|
||||
test(cdet, ++round, {23, 26, 28}, {26, 28}, {23}, {});
|
||||
|
||||
for (int i = 0; i != 10; ++i)
|
||||
test(cdet, ++round, {23}, {}, {23}, {});
|
||||
|
||||
test(cdet, ++round, {23, 29}, {29}, {23}, {});
|
||||
test(cdet, ++round, {30, 31}, {31}, {30}, {});
|
||||
test(cdet, ++round, {30}, {30}, {}, {});
|
||||
test(cdet, ++round, {}, {}, {}, {});
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(RCLCensorshipDetector, consensus, xrpl);
|
||||
} // namespace xrpl::test
|
||||
@@ -1,109 +0,0 @@
|
||||
#include <test/csf/PeerGroup.h>
|
||||
#include <test/csf/Sim.h>
|
||||
#include <test/csf/collectors.h>
|
||||
#include <test/csf/random.h>
|
||||
#include <test/csf/submitters.h>
|
||||
#include <test/csf/timers.h>
|
||||
|
||||
#include <xrpld/consensus/ConsensusParms.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <ostream>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class ScaleFreeSim_test : public beast::unit_test::Suite
|
||||
{
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace std::chrono;
|
||||
using namespace csf;
|
||||
|
||||
// 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);
|
||||
|
||||
BEAST_EXPECT(sim.branches() == 1);
|
||||
BEAST_EXPECT(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.?
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(ScaleFreeSim, consensus, xrpl, 80);
|
||||
|
||||
} // namespace xrpl::test
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,232 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/Digraph.h>
|
||||
#include <test/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
|
||||
@@ -1,134 +0,0 @@
|
||||
#include <test/csf/BasicNetwork.h>
|
||||
#include <test/csf/Scheduler.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class BasicNetwork_test : public beast::unit_test::Suite
|
||||
{
|
||||
public:
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
testNetwork()
|
||||
{
|
||||
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);
|
||||
BEAST_EXPECT(!net.connect(&pv[0], &pv[0]));
|
||||
BEAST_EXPECT(net.connect(&pv[0], &pv[1], 1s));
|
||||
BEAST_EXPECT(net.connect(&pv[1], &pv[2], 1s));
|
||||
BEAST_EXPECT(!net.connect(&pv[0], &pv[1]));
|
||||
for (auto& peer : pv)
|
||||
peer.start(scheduler, net);
|
||||
BEAST_EXPECT(scheduler.stepFor(0s));
|
||||
BEAST_EXPECT(scheduler.stepFor(1s));
|
||||
BEAST_EXPECT(scheduler.step());
|
||||
BEAST_EXPECT(!scheduler.step());
|
||||
BEAST_EXPECT(!scheduler.stepFor(1s));
|
||||
net.send(&pv[0], &pv[1], [] {});
|
||||
net.send(&pv[1], &pv[0], [] {});
|
||||
BEAST_EXPECT(net.disconnect(&pv[0], &pv[1]));
|
||||
BEAST_EXPECT(!net.disconnect(&pv[0], &pv[1]));
|
||||
for (;;)
|
||||
{
|
||||
auto const links = net.links(&pv[1]);
|
||||
if (links.empty())
|
||||
break;
|
||||
BEAST_EXPECT(net.disconnect(&pv[1], links[0].target));
|
||||
}
|
||||
BEAST_EXPECT(pv[0].set == std::set<int>({0, 2, 4}));
|
||||
BEAST_EXPECT(pv[1].set == std::set<int>({1, 3}));
|
||||
BEAST_EXPECT(pv[2].set == std::set<int>({2, 4}));
|
||||
}
|
||||
|
||||
void
|
||||
testDisconnect()
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
csf::Scheduler scheduler;
|
||||
csf::BasicNetwork<int> net(scheduler);
|
||||
BEAST_EXPECT(net.connect(0, 1, 1s));
|
||||
BEAST_EXPECT(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, [&]() { BEAST_EXPECT(net.disconnect(0, 2)); });
|
||||
scheduler.in(1100ms, [&]() { BEAST_EXPECT(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
|
||||
BEAST_EXPECT(delivered == std::set<int>({1}));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testNetwork();
|
||||
testDisconnect();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(BasicNetwork, csf, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1,333 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/Proposal.h>
|
||||
#include <test/csf/SimTime.h>
|
||||
#include <test/csf/Tx.h>
|
||||
#include <test/csf/Validation.h>
|
||||
#include <test/csf/events.h>
|
||||
#include <test/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
|
||||
@@ -1,238 +0,0 @@
|
||||
#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
|
||||
@@ -1,81 +0,0 @@
|
||||
#include <test/csf/Digraph.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class Digraph_test : public beast::unit_test::Suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace csf;
|
||||
using Graph = Digraph<char, std::string>;
|
||||
Graph graph;
|
||||
|
||||
BEAST_EXPECT(!graph.connected('a', 'b'));
|
||||
BEAST_EXPECT(!graph.edge('a', 'b'));
|
||||
BEAST_EXPECT(!graph.disconnect('a', 'b'));
|
||||
|
||||
BEAST_EXPECT(graph.connect('a', 'b', "foobar"));
|
||||
BEAST_EXPECT(graph.connected('a', 'b'));
|
||||
BEAST_EXPECT(
|
||||
*graph.edge('a', 'b') == "foobar"); // NOLINT(bugprone-unchecked-optional-access)
|
||||
|
||||
BEAST_EXPECT(!graph.connect('a', 'b', "repeat"));
|
||||
BEAST_EXPECT(graph.disconnect('a', 'b'));
|
||||
BEAST_EXPECT(graph.connect('a', 'b', "repeat"));
|
||||
BEAST_EXPECT(graph.connected('a', 'b'));
|
||||
BEAST_EXPECT(
|
||||
*graph.edge('a', 'b') == "repeat"); // NOLINT(bugprone-unchecked-optional-access)
|
||||
|
||||
BEAST_EXPECT(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");
|
||||
BEAST_EXPECT(edges == expected);
|
||||
BEAST_EXPECT(graph.outDegree('a') == expected.size());
|
||||
}
|
||||
|
||||
BEAST_EXPECT(graph.outEdges('r').size() == 0);
|
||||
BEAST_EXPECT(graph.outDegree('r') == 0);
|
||||
BEAST_EXPECT(graph.outDegree('c') == 0);
|
||||
|
||||
// only 'a' has out edges
|
||||
BEAST_EXPECT(graph.outVertices().size() == 1);
|
||||
std::vector<char> const expected = {'b', 'c'};
|
||||
|
||||
BEAST_EXPECT((graph.outVertices('a') == expected));
|
||||
BEAST_EXPECT(graph.outVertices('b').size() == 0);
|
||||
BEAST_EXPECT(graph.outVertices('c').size() == 0);
|
||||
BEAST_EXPECT(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";
|
||||
BEAST_EXPECT(ss.str() == expectedDot);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Digraph, csf, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1,121 +0,0 @@
|
||||
#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
|
||||
@@ -1,66 +0,0 @@
|
||||
#include <test/csf/Histogram.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class Histogram_test : public beast::unit_test::Suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace csf;
|
||||
Histogram<int> hist;
|
||||
|
||||
BEAST_EXPECT(hist.size() == 0);
|
||||
BEAST_EXPECT(hist.numBins() == 0);
|
||||
BEAST_EXPECT(hist.minValue() == 0);
|
||||
BEAST_EXPECT(hist.maxValue() == 0);
|
||||
BEAST_EXPECT(hist.avg() == 0);
|
||||
BEAST_EXPECT(hist.percentile(0.0f) == hist.minValue());
|
||||
BEAST_EXPECT(hist.percentile(0.5f) == 0);
|
||||
BEAST_EXPECT(hist.percentile(0.9f) == 0);
|
||||
BEAST_EXPECT(hist.percentile(1.0f) == hist.maxValue());
|
||||
|
||||
hist.insert(1);
|
||||
|
||||
BEAST_EXPECT(hist.size() == 1);
|
||||
BEAST_EXPECT(hist.numBins() == 1);
|
||||
BEAST_EXPECT(hist.minValue() == 1);
|
||||
BEAST_EXPECT(hist.maxValue() == 1);
|
||||
BEAST_EXPECT(hist.avg() == 1);
|
||||
BEAST_EXPECT(hist.percentile(0.0f) == hist.minValue());
|
||||
BEAST_EXPECT(hist.percentile(0.5f) == 1);
|
||||
BEAST_EXPECT(hist.percentile(0.9f) == 1);
|
||||
BEAST_EXPECT(hist.percentile(1.0f) == hist.maxValue());
|
||||
|
||||
hist.insert(9);
|
||||
|
||||
BEAST_EXPECT(hist.size() == 2);
|
||||
BEAST_EXPECT(hist.numBins() == 2);
|
||||
BEAST_EXPECT(hist.minValue() == 1);
|
||||
BEAST_EXPECT(hist.maxValue() == 9);
|
||||
BEAST_EXPECT(hist.avg() == 5);
|
||||
BEAST_EXPECT(hist.percentile(0.0f) == hist.minValue());
|
||||
BEAST_EXPECT(hist.percentile(0.5f) == 1);
|
||||
BEAST_EXPECT(hist.percentile(0.9f) == 9);
|
||||
BEAST_EXPECT(hist.percentile(1.0f) == hist.maxValue());
|
||||
|
||||
hist.insert(1);
|
||||
|
||||
BEAST_EXPECT(hist.size() == 3);
|
||||
BEAST_EXPECT(hist.numBins() == 2);
|
||||
BEAST_EXPECT(hist.minValue() == 1);
|
||||
BEAST_EXPECT(hist.maxValue() == 9);
|
||||
BEAST_EXPECT(hist.avg() == 11 / 3);
|
||||
BEAST_EXPECT(hist.percentile(0.0f) == hist.minValue());
|
||||
BEAST_EXPECT(hist.percentile(0.5f) == 1);
|
||||
BEAST_EXPECT(hist.percentile(0.9f) == 9);
|
||||
BEAST_EXPECT(hist.percentile(1.0f) == hist.maxValue());
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Histogram, csf, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
1024
src/test/csf/Peer.h
1024
src/test/csf/Peer.h
File diff suppressed because it is too large
Load Diff
@@ -1,351 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/Peer.h>
|
||||
#include <test/csf/SimTime.h>
|
||||
#include <test/csf/Validation.h>
|
||||
#include <test/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
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/Tx.h>
|
||||
#include <test/csf/Validation.h>
|
||||
#include <test/csf/ledgers.h>
|
||||
|
||||
#include <xrpld/consensus/ConsensusProposal.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
|
||||
@@ -1,192 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,443 +0,0 @@
|
||||
#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
|
||||
@@ -1,68 +0,0 @@
|
||||
#include <test/csf/Scheduler.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
|
||||
#include <set>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class Scheduler_test : public beast::unit_test::Suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
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
|
||||
BEAST_EXPECT(seen.empty());
|
||||
BEAST_EXPECT(scheduler.stepOne());
|
||||
BEAST_EXPECT(seen == std::set<int>({1}));
|
||||
BEAST_EXPECT(scheduler.now() == (start + 1s));
|
||||
|
||||
// No processing if stepping until current time
|
||||
BEAST_EXPECT(scheduler.stepUntil(scheduler.now()));
|
||||
BEAST_EXPECT(seen == std::set<int>({1}));
|
||||
BEAST_EXPECT(scheduler.now() == (start + 1s));
|
||||
|
||||
// Process next event
|
||||
BEAST_EXPECT(scheduler.stepFor(1s));
|
||||
BEAST_EXPECT(seen == std::set<int>({1, 2}));
|
||||
BEAST_EXPECT(scheduler.now() == (start + 2s));
|
||||
|
||||
// Don't process cancelled event, but advance clock
|
||||
scheduler.cancel(token);
|
||||
BEAST_EXPECT(scheduler.stepFor(1s));
|
||||
BEAST_EXPECT(seen == std::set<int>({1, 2}));
|
||||
BEAST_EXPECT(scheduler.now() == (start + 3s));
|
||||
|
||||
// Process until 3 seen ints
|
||||
BEAST_EXPECT(scheduler.stepWhile([&]() { return seen.size() < 3; }));
|
||||
BEAST_EXPECT(seen == std::set<int>({1, 2, 4}));
|
||||
BEAST_EXPECT(scheduler.now() == (start + 4s));
|
||||
|
||||
// Process the rest
|
||||
BEAST_EXPECT(scheduler.step());
|
||||
BEAST_EXPECT(seen == std::set<int>({1, 2, 4, 8}));
|
||||
BEAST_EXPECT(scheduler.now() == (start + 8s));
|
||||
|
||||
// Process the rest again doesn't advance
|
||||
BEAST_EXPECT(!scheduler.step());
|
||||
BEAST_EXPECT(seen == std::set<int>({1, 2, 4, 8}));
|
||||
BEAST_EXPECT(scheduler.now() == (start + 8s));
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Scheduler, csf, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1,171 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/BasicNetwork.h>
|
||||
#include <test/csf/CollectorRef.h>
|
||||
#include <test/csf/Peer.h>
|
||||
#include <test/csf/PeerGroup.h>
|
||||
#include <test/csf/Scheduler.h>
|
||||
#include <test/csf/SimTime.h>
|
||||
#include <test/csf/TrustGraph.h>
|
||||
#include <test/csf/ledgers.h>
|
||||
|
||||
#include <xrpl/beast/utility/Journal.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
|
||||
@@ -1,17 +0,0 @@
|
||||
#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
|
||||
@@ -1,152 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/Digraph.h>
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
|
||||
#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
|
||||
@@ -1,228 +0,0 @@
|
||||
#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
|
||||
@@ -1,177 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/ledgers.h>
|
||||
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/tagged_integer.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
|
||||
@@ -1,671 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/Histogram.h>
|
||||
#include <test/csf/SimTime.h>
|
||||
#include <test/csf/Tx.h>
|
||||
#include <test/csf/Validation.h>
|
||||
#include <test/csf/events.h>
|
||||
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test::csf {
|
||||
|
||||
// A collector is any class that implements
|
||||
//
|
||||
// on(NodeID, SimTime, Event)
|
||||
//
|
||||
// for all events emitted by a Peer.
|
||||
//
|
||||
// This file contains helper functions for composing different collectors
|
||||
// and also defines several standard collectors available for simulations.
|
||||
|
||||
/**
|
||||
* Group of collectors.
|
||||
*
|
||||
* Presents a group of collectors as a single collector which process an event
|
||||
* by calling each collector sequentially. This is analogous to CollectorRefs
|
||||
* in CollectorRef.h, but does *not* erase the type information of the combined
|
||||
* collectors.
|
||||
*/
|
||||
template <class... Cs>
|
||||
class Collectors
|
||||
{
|
||||
std::tuple<Cs&...> cs_;
|
||||
|
||||
template <class C, class E>
|
||||
static void
|
||||
apply(C& c, PeerID who, SimTime when, E e)
|
||||
{
|
||||
c.on(who, when, e);
|
||||
}
|
||||
|
||||
template <std::size_t... Is, class E>
|
||||
static void
|
||||
apply(std::tuple<Cs&...>& cs, PeerID who, SimTime when, E e, std::index_sequence<Is...>)
|
||||
{
|
||||
(..., apply(std::get<Is>(cs), who, when, e));
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param cs References to the collectors to call together
|
||||
*/
|
||||
Collectors(Cs&... cs) : cs_(std::tie(cs...))
|
||||
{
|
||||
}
|
||||
|
||||
template <class E>
|
||||
void
|
||||
on(PeerID who, SimTime when, E e)
|
||||
{
|
||||
apply(cs_, who, when, e, std::index_sequence_for<Cs...>{});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an instance of Collectors<Cs...>
|
||||
*/
|
||||
template <class... Cs>
|
||||
Collectors<Cs...>
|
||||
makeCollectors(Cs&... cs)
|
||||
{
|
||||
return Collectors<Cs...>(cs...);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintain an instance of a Collector per peer
|
||||
*
|
||||
* For each peer that emits events, this class maintains a corresponding
|
||||
* instance of CollectorType, only forwarding events emitted by the peer to
|
||||
* the related instance.
|
||||
*
|
||||
* CollectorType should be default constructible.
|
||||
*/
|
||||
template <class CollectorType>
|
||||
struct CollectByNode
|
||||
{
|
||||
std::map<PeerID, CollectorType> byNode;
|
||||
|
||||
CollectorType&
|
||||
operator[](PeerID who)
|
||||
{
|
||||
return byNode[who];
|
||||
}
|
||||
|
||||
CollectorType const&
|
||||
operator[](PeerID who) const
|
||||
{
|
||||
return byNode[who];
|
||||
}
|
||||
template <class E>
|
||||
void
|
||||
on(PeerID who, SimTime when, E const& e)
|
||||
{
|
||||
byNode[who].on(who, when, e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Collector which ignores all events
|
||||
*/
|
||||
struct NullCollector
|
||||
{
|
||||
template <class E>
|
||||
void
|
||||
on(PeerID, SimTime, E const& e)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks the overall duration of a simulation
|
||||
*/
|
||||
struct SimDurationCollector
|
||||
{
|
||||
bool init = false;
|
||||
SimTime start;
|
||||
SimTime stop;
|
||||
|
||||
template <class E>
|
||||
void
|
||||
on(PeerID, SimTime when, E const& e)
|
||||
{
|
||||
if (!init)
|
||||
{
|
||||
start = when;
|
||||
init = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
stop = when;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks the submission -> accepted -> validated evolution of transactions.
|
||||
*
|
||||
* This collector tracks transactions through the network by monitoring the
|
||||
* *first* time the transaction is seen by any node in the network, or
|
||||
* seen by any node's accepted or fully validated ledger.
|
||||
*
|
||||
* If transactions submitted to the network do not have unique IDs, this
|
||||
* collector will not track subsequent submissions.
|
||||
*/
|
||||
struct TxCollector
|
||||
{
|
||||
// Counts
|
||||
std::size_t submitted{0};
|
||||
std::size_t accepted{0};
|
||||
std::size_t validated{0};
|
||||
|
||||
struct Tracker
|
||||
{
|
||||
Tx tx;
|
||||
SimTime submitted;
|
||||
std::optional<SimTime> accepted;
|
||||
std::optional<SimTime> validated;
|
||||
|
||||
Tracker(Tx tx, SimTime submitted) : tx{tx}, submitted{submitted}
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
hash_map<Tx::ID, Tracker> txs;
|
||||
|
||||
using Hist = Histogram<SimTime::duration>;
|
||||
Hist submitToAccept;
|
||||
Hist submitToValidate;
|
||||
|
||||
// Ignore most events by default
|
||||
template <class E>
|
||||
void
|
||||
on(PeerID, SimTime when, E const& e)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
on(PeerID who, SimTime when, SubmitTx const& e)
|
||||
{
|
||||
// save first time it was seen
|
||||
if (txs.emplace(e.tx.id(), Tracker{e.tx, when}).second)
|
||||
{
|
||||
submitted++;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
on(PeerID who, SimTime when, AcceptLedger const& e)
|
||||
{
|
||||
for (auto const& tx : e.ledger.txs())
|
||||
{
|
||||
auto it = txs.find(tx.id());
|
||||
if (it != txs.end() && !it->second.accepted)
|
||||
{
|
||||
Tracker& tracker = it->second;
|
||||
tracker.accepted = when;
|
||||
accepted++;
|
||||
|
||||
submitToAccept.insert(*tracker.accepted - tracker.submitted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
on(PeerID who, SimTime when, FullyValidateLedger const& e)
|
||||
{
|
||||
for (auto const& tx : e.ledger.txs())
|
||||
{
|
||||
auto it = txs.find(tx.id());
|
||||
if (it != txs.end() && !it->second.validated)
|
||||
{
|
||||
Tracker& tracker = it->second;
|
||||
// Should only validated a previously accepted Tx
|
||||
assert(tracker.accepted);
|
||||
|
||||
tracker.validated = when;
|
||||
validated++;
|
||||
submitToValidate.insert(*tracker.validated - tracker.submitted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the number of txs which were never accepted
|
||||
[[nodiscard]] std::size_t
|
||||
orphaned() const
|
||||
{
|
||||
return std::count_if(
|
||||
txs.begin(), txs.end(), [](auto const& it) { return !it.second.accepted; });
|
||||
}
|
||||
|
||||
// Returns the number of txs which were never validated
|
||||
[[nodiscard]] std::size_t
|
||||
unvalidated() const
|
||||
{
|
||||
return std::count_if(
|
||||
txs.begin(), txs.end(), [](auto const& it) { return !it.second.validated; });
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void
|
||||
report(SimDuration simDuration, T& log, bool printBreakline = false)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
auto perSec = [&simDuration](std::size_t count) {
|
||||
return double(count) / duration_cast<seconds>(simDuration).count();
|
||||
};
|
||||
|
||||
auto fmtS = [](SimDuration dur) { return duration_cast<duration<float>>(dur).count(); };
|
||||
|
||||
if (printBreakline)
|
||||
{
|
||||
log << std::setw(11) << std::setfill('-') << "-" << "-" << std::setw(7)
|
||||
<< std::setfill('-') << "-" << "-" << std::setw(7) << std::setfill('-') << "-"
|
||||
<< "-" << std::setw(36) << std::setfill('-') << "-" << std::endl;
|
||||
log << std::setfill(' ');
|
||||
}
|
||||
|
||||
log << std::left << std::setw(11) << "TxStats" << "|" << std::setw(7) << "Count" << "|"
|
||||
<< std::setw(7) << "Per Sec" << "|" << std::setw(15) << "Latency (sec)" << std::right
|
||||
<< std::setw(7) << "10-ile" << std::setw(7) << "50-ile" << std::setw(7) << "90-ile"
|
||||
<< std::left << std::endl;
|
||||
|
||||
log << std::setw(11) << std::setfill('-') << "-" << "|" << std::setw(7) << std::setfill('-')
|
||||
<< "-" << "|" << std::setw(7) << std::setfill('-') << "-" << "|" << std::setw(36)
|
||||
<< std::setfill('-') << "-" << std::endl;
|
||||
log << std::setfill(' ');
|
||||
|
||||
log << std::left << std::setw(11) << "Submit " << "|" << std::right << std::setw(7)
|
||||
<< submitted << "|" << std::setw(7) << std::setprecision(2) << perSec(submitted) << "|"
|
||||
<< std::setw(36) << "" << std::endl;
|
||||
|
||||
log << std::left << std::setw(11) << "Accept " << "|" << std::right << std::setw(7)
|
||||
<< accepted << "|" << std::setw(7) << std::setprecision(2) << perSec(accepted) << "|"
|
||||
<< std::setw(15) << std::left << "From Submit" << std::right << std::setw(7)
|
||||
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.1f)) << std::setw(7)
|
||||
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.5f)) << std::setw(7)
|
||||
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.9f)) << std::endl;
|
||||
|
||||
log << std::left << std::setw(11) << "Validate " << "|" << std::right << std::setw(7)
|
||||
<< validated << "|" << std::setw(7) << std::setprecision(2) << perSec(validated) << "|"
|
||||
<< std::setw(15) << std::left << "From Submit" << std::right << std::setw(7)
|
||||
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.1f)) << std::setw(7)
|
||||
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.5f)) << std::setw(7)
|
||||
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.9f)) << std::endl;
|
||||
|
||||
log << std::left << std::setw(11) << "Orphan" << "|" << std::right << std::setw(7)
|
||||
<< orphaned() << "|" << std::setw(7) << "" << "|" << std::setw(36) << std::endl;
|
||||
|
||||
log << std::left << std::setw(11) << "Unvalidated" << "|" << std::right << std::setw(7)
|
||||
<< unvalidated() << "|" << std::setw(7) << "" << "|" << std::setw(43) << std::endl;
|
||||
|
||||
log << std::setw(11) << std::setfill('-') << "-" << "-" << std::setw(7) << std::setfill('-')
|
||||
<< "-" << "-" << std::setw(7) << std::setfill('-') << "-" << "-" << std::setw(36)
|
||||
<< std::setfill('-') << "-" << std::endl;
|
||||
log << std::setfill(' ');
|
||||
}
|
||||
|
||||
template <class T, class Tag>
|
||||
void
|
||||
csv(SimDuration simDuration, T& log, Tag const& tag, bool printHeaders = false)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
auto perSec = [&simDuration](std::size_t count) {
|
||||
return double(count) / duration_cast<seconds>(simDuration).count();
|
||||
};
|
||||
|
||||
auto fmtS = [](SimDuration dur) { return duration_cast<duration<float>>(dur).count(); };
|
||||
|
||||
if (printHeaders)
|
||||
{
|
||||
log << "tag" << "," << "txNumSubmitted" << "," << "txNumAccepted"
|
||||
<< "," << "txNumValidated" << "," << "txNumOrphaned" << ","
|
||||
<< "txUnvalidated" << "," << "txRateSumbitted" << ","
|
||||
<< "txRateAccepted" << "," << "txRateValidated" << ","
|
||||
<< "txLatencySubmitToAccept10Pctl" << ","
|
||||
<< "txLatencySubmitToAccept50Pctl" << ","
|
||||
<< "txLatencySubmitToAccept90Pctl" << ","
|
||||
<< "txLatencySubmitToValidatet10Pctl" << ","
|
||||
<< "txLatencySubmitToValidatet50Pctl" << ","
|
||||
<< "txLatencySubmitToValidatet90Pctl" << std::endl;
|
||||
}
|
||||
|
||||
log << tag
|
||||
<< ","
|
||||
// txNumSubmitted
|
||||
<< submitted
|
||||
<< ","
|
||||
// txNumAccepted
|
||||
<< accepted
|
||||
<< ","
|
||||
// txNumValidated
|
||||
<< validated
|
||||
<< ","
|
||||
// txNumOrphaned
|
||||
<< orphaned()
|
||||
<< ","
|
||||
// txNumUnvalidated
|
||||
<< unvalidated()
|
||||
<< ","
|
||||
// txRateSubmitted
|
||||
<< std::setprecision(2) << perSec(submitted)
|
||||
<< ","
|
||||
// txRateAccepted
|
||||
<< std::setprecision(2) << perSec(accepted)
|
||||
<< ","
|
||||
// txRateValidated
|
||||
<< std::setprecision(2) << perSec(validated)
|
||||
<< ","
|
||||
// txLatencySubmitToAccept10Pctl
|
||||
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.1f))
|
||||
<< ","
|
||||
// txLatencySubmitToAccept50Pctl
|
||||
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.5f))
|
||||
<< ","
|
||||
// txLatencySubmitToAccept90Pctl
|
||||
<< std::setprecision(2) << fmtS(submitToAccept.percentile(0.9f))
|
||||
<< ","
|
||||
// txLatencySubmitToValidate10Pctl
|
||||
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.1f))
|
||||
<< ","
|
||||
// txLatencySubmitToValidate50Pctl
|
||||
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.5f))
|
||||
<< ","
|
||||
// txLatencySubmitToValidate90Pctl
|
||||
<< std::setprecision(2) << fmtS(submitToValidate.percentile(0.9f)) << "," << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks the accepted -> validated evolution of ledgers.
|
||||
*
|
||||
* This collector tracks ledgers through the network by monitoring the
|
||||
* *first* time the ledger is accepted or fully validated by ANY node.
|
||||
*/
|
||||
struct LedgerCollector
|
||||
{
|
||||
std::size_t accepted{0};
|
||||
std::size_t fullyValidated{0};
|
||||
|
||||
struct Tracker
|
||||
{
|
||||
SimTime accepted;
|
||||
std::optional<SimTime> fullyValidated;
|
||||
|
||||
Tracker(SimTime accepted) : accepted{accepted}
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
hash_map<Ledger::ID, Tracker> ledgers;
|
||||
|
||||
using Hist = Histogram<SimTime::duration>;
|
||||
Hist acceptToFullyValid;
|
||||
Hist acceptToAccept;
|
||||
Hist fullyValidToFullyValid;
|
||||
|
||||
// Ignore most events by default
|
||||
template <class E>
|
||||
void
|
||||
on(PeerID, SimTime, E const& e)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
on(PeerID who, SimTime when, AcceptLedger const& e)
|
||||
{
|
||||
// First time this ledger accepted
|
||||
if (ledgers.emplace(e.ledger.id(), Tracker{when}).second)
|
||||
{
|
||||
++accepted;
|
||||
// ignore jumps?
|
||||
if (e.prior.id() == e.ledger.parentID())
|
||||
{
|
||||
auto const it = ledgers.find(e.ledger.parentID());
|
||||
if (it != ledgers.end())
|
||||
{
|
||||
acceptToAccept.insert(when - it->second.accepted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
on(PeerID who, SimTime when, FullyValidateLedger const& e)
|
||||
{
|
||||
// ignore jumps
|
||||
if (e.prior.id() == e.ledger.parentID())
|
||||
{
|
||||
auto const it = ledgers.find(e.ledger.id());
|
||||
assert(it != ledgers.end());
|
||||
auto& tracker = it->second;
|
||||
// first time fully validated
|
||||
if (!tracker.fullyValidated)
|
||||
{
|
||||
++fullyValidated;
|
||||
tracker.fullyValidated = when;
|
||||
acceptToFullyValid.insert(when - tracker.accepted);
|
||||
|
||||
auto const parentIt = ledgers.find(e.ledger.parentID());
|
||||
if (parentIt != ledgers.end())
|
||||
{
|
||||
auto& parentTracker = parentIt->second;
|
||||
if (parentTracker.fullyValidated)
|
||||
{
|
||||
fullyValidToFullyValid.insert(when - *parentTracker.fullyValidated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t
|
||||
unvalidated() const
|
||||
{
|
||||
return std::count_if(ledgers.begin(), ledgers.end(), [](auto const& it) {
|
||||
return !it.second.fullyValidated;
|
||||
});
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void
|
||||
report(SimDuration simDuration, T& log, bool printBreakline = false)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
auto perSec = [&simDuration](std::size_t count) {
|
||||
return double(count) / duration_cast<seconds>(simDuration).count();
|
||||
};
|
||||
|
||||
auto fmtS = [](SimDuration dur) { return duration_cast<duration<float>>(dur).count(); };
|
||||
|
||||
if (printBreakline)
|
||||
{
|
||||
log << std::setw(11) << std::setfill('-') << "-" << "-" << std::setw(7)
|
||||
<< std::setfill('-') << "-" << "-" << std::setw(7) << std::setfill('-') << "-"
|
||||
<< "-" << std::setw(36) << std::setfill('-') << "-" << std::endl;
|
||||
log << std::setfill(' ');
|
||||
}
|
||||
|
||||
log << std::left << std::setw(11) << "LedgerStats" << "|" << std::setw(7) << "Count" << "|"
|
||||
<< std::setw(7) << "Per Sec"
|
||||
<< "|" << std::setw(15) << "Latency (sec)" << std::right << std::setw(7) << "10-ile"
|
||||
<< std::setw(7) << "50-ile" << std::setw(7) << "90-ile" << std::left << std::endl;
|
||||
|
||||
log << std::setw(11) << std::setfill('-') << "-" << "|" << std::setw(7) << std::setfill('-')
|
||||
<< "-" << "|" << std::setw(7) << std::setfill('-') << "-" << "|" << std::setw(36)
|
||||
<< std::setfill('-') << "-" << std::endl;
|
||||
log << std::setfill(' ');
|
||||
|
||||
log << std::left << std::setw(11) << "Accept " << "|" << std::right << std::setw(7)
|
||||
<< accepted << "|" << std::setw(7) << std::setprecision(2) << perSec(accepted) << "|"
|
||||
<< std::setw(15) << std::left << "From Accept" << std::right << std::setw(7)
|
||||
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.1f)) << std::setw(7)
|
||||
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.5f)) << std::setw(7)
|
||||
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.9f)) << std::endl;
|
||||
|
||||
log << std::left << std::setw(11) << "Validate " << "|" << std::right << std::setw(7)
|
||||
<< fullyValidated << "|" << std::setw(7) << std::setprecision(2)
|
||||
<< perSec(fullyValidated) << "|" << std::setw(15) << std::left << "From Validate "
|
||||
<< std::right << std::setw(7) << std::setprecision(2)
|
||||
<< fmtS(fullyValidToFullyValid.percentile(0.1f)) << std::setw(7) << std::setprecision(2)
|
||||
<< fmtS(fullyValidToFullyValid.percentile(0.5f)) << std::setw(7) << std::setprecision(2)
|
||||
<< fmtS(fullyValidToFullyValid.percentile(0.9f)) << std::endl;
|
||||
|
||||
log << std::setw(11) << std::setfill('-') << "-" << "-" << std::setw(7) << std::setfill('-')
|
||||
<< "-" << "-" << std::setw(7) << std::setfill('-') << "-" << "-" << std::setw(36)
|
||||
<< std::setfill('-') << "-" << std::endl;
|
||||
log << std::setfill(' ');
|
||||
}
|
||||
|
||||
template <class T, class Tag>
|
||||
void
|
||||
csv(SimDuration simDuration, T& log, Tag const& tag, bool printHeaders = false)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
auto perSec = [&simDuration](std::size_t count) {
|
||||
return double(count) / duration_cast<seconds>(simDuration).count();
|
||||
};
|
||||
|
||||
auto fmtS = [](SimDuration dur) { return duration_cast<duration<float>>(dur).count(); };
|
||||
|
||||
if (printHeaders)
|
||||
{
|
||||
log << "tag" << "," << "ledgerNumAccepted" << ","
|
||||
<< "ledgerNumFullyValidated" << "," << "ledgerRateAccepted"
|
||||
<< "," << "ledgerRateFullyValidated" << ","
|
||||
<< "ledgerLatencyAcceptToAccept10Pctl" << ","
|
||||
<< "ledgerLatencyAcceptToAccept50Pctl" << ","
|
||||
<< "ledgerLatencyAcceptToAccept90Pctl" << ","
|
||||
<< "ledgerLatencyFullyValidToFullyValid10Pctl" << ","
|
||||
<< "ledgerLatencyFullyValidToFullyValid50Pctl" << ","
|
||||
<< "ledgerLatencyFullyValidToFullyValid90Pctl" << std::endl;
|
||||
}
|
||||
|
||||
log << tag
|
||||
<< ","
|
||||
// ledgerNumAccepted
|
||||
<< accepted
|
||||
<< ","
|
||||
// ledgerNumFullyValidated
|
||||
<< fullyValidated
|
||||
<< ","
|
||||
// ledgerRateAccepted
|
||||
<< std::setprecision(2) << perSec(accepted)
|
||||
<< ","
|
||||
// ledgerRateFullyValidated
|
||||
<< std::setprecision(2) << perSec(fullyValidated)
|
||||
<< ","
|
||||
// ledgerLatencyAcceptToAccept10Pctl
|
||||
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.1f))
|
||||
<< ","
|
||||
// ledgerLatencyAcceptToAccept50Pctl
|
||||
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.5f))
|
||||
<< ","
|
||||
// ledgerLatencyAcceptToAccept90Pctl
|
||||
<< std::setprecision(2) << fmtS(acceptToAccept.percentile(0.9f))
|
||||
<< ","
|
||||
// ledgerLatencyFullyValidToFullyValid10Pctl
|
||||
<< std::setprecision(2) << fmtS(fullyValidToFullyValid.percentile(0.1f))
|
||||
<< ","
|
||||
// ledgerLatencyFullyValidToFullyValid50Pctl
|
||||
<< std::setprecision(2) << fmtS(fullyValidToFullyValid.percentile(0.5f))
|
||||
<< ","
|
||||
// ledgerLatencyFullyValidToFullyValid90Pctl
|
||||
<< std::setprecision(2) << fmtS(fullyValidToFullyValid.percentile(0.9f)) << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Write out stream of ledger activity
|
||||
*
|
||||
* Writes information about every accepted and fully-validated ledger to a
|
||||
* provided std::ostream.
|
||||
*/
|
||||
struct StreamCollector
|
||||
{
|
||||
std::ostream& out;
|
||||
|
||||
// Ignore most events by default
|
||||
template <class E>
|
||||
void
|
||||
on(PeerID, SimTime, E const& e)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
on(PeerID who, SimTime when, AcceptLedger const& e)
|
||||
{
|
||||
out << when.time_since_epoch().count() << ": Node " << who << " accepted " << "L"
|
||||
<< e.ledger.id() << " " << e.ledger.txs() << "\n";
|
||||
}
|
||||
|
||||
void
|
||||
on(PeerID who, SimTime when, FullyValidateLedger const& e)
|
||||
{
|
||||
out << when.time_since_epoch().count() << ": Node " << who << " fully-validated " << "L"
|
||||
<< e.ledger.id() << " " << e.ledger.txs() << "\n";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves information about Jumps for closed and fully validated ledgers. A
|
||||
* jump occurs when a node closes/fully validates a new ledger that is not the
|
||||
* immediate child of the prior closed/fully validated ledgers. This includes
|
||||
* jumps across branches and jumps ahead in the same branch of ledger history.
|
||||
*/
|
||||
struct JumpCollector
|
||||
{
|
||||
struct Jump
|
||||
{
|
||||
PeerID id;
|
||||
SimTime when;
|
||||
Ledger from;
|
||||
Ledger to;
|
||||
};
|
||||
|
||||
std::vector<Jump> closeJumps;
|
||||
std::vector<Jump> fullyValidatedJumps;
|
||||
|
||||
// Ignore most events by default
|
||||
template <class E>
|
||||
void
|
||||
on(PeerID, SimTime, E const& e)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
on(PeerID who, SimTime when, AcceptLedger const& e)
|
||||
{
|
||||
// Not a direct child -> parent switch
|
||||
if (e.ledger.parentID() != e.prior.id())
|
||||
closeJumps.emplace_back(Jump{.id = who, .when = when, .from = e.prior, .to = e.ledger});
|
||||
}
|
||||
|
||||
void
|
||||
on(PeerID who, SimTime when, FullyValidateLedger const& e)
|
||||
{
|
||||
// Not a direct child -> parent switch
|
||||
if (e.ledger.parentID() != e.prior.id())
|
||||
{
|
||||
fullyValidatedJumps.emplace_back(
|
||||
Jump{.id = who, .when = when, .from = e.prior, .to = e.ledger});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::test::csf
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 78 KiB |
@@ -1,157 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/Tx.h>
|
||||
#include <test/csf/Validation.h>
|
||||
#include <test/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
|
||||
@@ -1,70 +0,0 @@
|
||||
#include <test/csf/Sim.h>
|
||||
|
||||
#include <test/csf/PeerGroup.h>
|
||||
#include <test/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
|
||||
@@ -1,167 +0,0 @@
|
||||
#include <test/csf/ledgers.h>
|
||||
|
||||
#include <test/csf/Tx.h>
|
||||
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/ledger/LedgerTiming.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
|
||||
@@ -1,360 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/Tx.h>
|
||||
|
||||
#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 <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
|
||||
@@ -1,158 +0,0 @@
|
||||
#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_;
|
||||
std::uniform_real_distribution<double> uf_{0, 1};
|
||||
|
||||
public:
|
||||
using result_type = double;
|
||||
|
||||
PowerLawDistribution(double xmin, double a) : xmin_{xmin}, a_{a}, inv_(1.0 / (1.0 - 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
|
||||
@@ -1,111 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/Scheduler.h>
|
||||
#include <test/csf/SimTime.h>
|
||||
#include <test/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
|
||||
@@ -1,63 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/csf/Scheduler.h>
|
||||
#include <test/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
|
||||
@@ -514,6 +514,49 @@ public:
|
||||
parseFailureExpected_ = b;
|
||||
}
|
||||
|
||||
/**
|
||||
* RAII class to set and restore the parse failure flag (setParseFailureExpected).
|
||||
*
|
||||
* Can be created directly, or through the `getParseFailureGuard(bool)` function.
|
||||
*/
|
||||
class ParseFailureGuard final
|
||||
{
|
||||
Env& self_;
|
||||
bool const oldExpected_;
|
||||
|
||||
public:
|
||||
ParseFailureGuard(Env& self, bool b)
|
||||
: self_(self), oldExpected_(self_.parseFailureExpected_)
|
||||
{
|
||||
self_.setParseFailureExpected(b);
|
||||
}
|
||||
|
||||
~ParseFailureGuard()
|
||||
{
|
||||
self_.setParseFailureExpected(oldExpected_);
|
||||
}
|
||||
|
||||
// No copy, no move
|
||||
ParseFailureGuard(ParseFailureGuard const&) = delete;
|
||||
ParseFailureGuard&
|
||||
operator=(ParseFailureGuard const&) = delete;
|
||||
ParseFailureGuard(ParseFailureGuard&& other) = delete;
|
||||
ParseFailureGuard&
|
||||
operator=(ParseFailureGuard&&) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an RAII guard to set and restore the parse failure flag
|
||||
*
|
||||
* Usage:
|
||||
* auto const guard = env.getParseFailureGuard(true/false);
|
||||
*/
|
||||
[[nodiscard]] ParseFailureGuard
|
||||
getParseFailureGuard(bool b)
|
||||
{
|
||||
return ParseFailureGuard{*this, b};
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn off signature checks.
|
||||
*/
|
||||
|
||||
@@ -629,7 +629,7 @@ Env::autofill(JTx& jt)
|
||||
catch (ParseError const&)
|
||||
{
|
||||
if (!parseFailureExpected_)
|
||||
test.log << "parse failed:\n" << pretty(jv) << std::endl;
|
||||
test.log << "parse failure:\n" << pretty(jv) << std::endl;
|
||||
rethrow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <xrpld/overlay/detail/PeerImp.h>
|
||||
#include <xrpld/overlay/detail/ProtocolVersion.h>
|
||||
#include <xrpld/overlay/detail/Tuning.h>
|
||||
#include <xrpld/peerfinder/Slot.h>
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
@@ -16,6 +15,7 @@
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/nodestore/NodeObject.h>
|
||||
#include <xrpl/peerfinder/Slot.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
#include <xrpld/overlay/detail/OverlayImpl.h>
|
||||
#include <xrpld/overlay/detail/PeerImp.h>
|
||||
#include <xrpld/overlay/detail/ProtocolVersion.h>
|
||||
#include <xrpld/peerfinder/Slot.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/make_SSLContext.h>
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/peerfinder/Slot.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
#include <test/beast/IPEndpointCommon.h>
|
||||
#include <test/unit_test/SuiteJournal.h>
|
||||
|
||||
#include <xrpld/peerfinder/PeerfinderManager.h>
|
||||
#include <xrpld/peerfinder/detail/Livecache.h>
|
||||
#include <xrpld/peerfinder/detail/Tuning.h>
|
||||
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
#include <xrpl/beast/unit_test/suite.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 <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::PeerFinder {
|
||||
|
||||
bool
|
||||
operator==(Endpoint const& a, Endpoint const& b)
|
||||
{
|
||||
return (a.hops == b.hops && a.address == b.address);
|
||||
}
|
||||
|
||||
class Livecache_test : public beast::unit_test::Suite
|
||||
{
|
||||
TestStopwatch clock_;
|
||||
test::SuiteJournal journal_;
|
||||
|
||||
public:
|
||||
Livecache_test() : journal_("Livecache_test", *this)
|
||||
{
|
||||
}
|
||||
|
||||
// Add the address as an endpoint
|
||||
template <class C>
|
||||
void
|
||||
add(beast::IP::Endpoint ep, C& c, std::uint32_t hops = 0)
|
||||
{
|
||||
Endpoint const cep{ep, hops};
|
||||
c.insert(cep);
|
||||
}
|
||||
|
||||
void
|
||||
testBasicInsert()
|
||||
{
|
||||
testcase("Basic Insert");
|
||||
Livecache<> c(clock_, journal_);
|
||||
BEAST_EXPECT(c.empty());
|
||||
|
||||
for (auto i = 0; i < 10; ++i)
|
||||
add(beast::IP::randomEP(true), c);
|
||||
|
||||
BEAST_EXPECT(!c.empty());
|
||||
BEAST_EXPECT(c.size() == 10);
|
||||
|
||||
for (auto i = 0; i < 10; ++i)
|
||||
add(beast::IP::randomEP(false), c);
|
||||
|
||||
BEAST_EXPECT(!c.empty());
|
||||
BEAST_EXPECT(c.size() == 20);
|
||||
}
|
||||
|
||||
void
|
||||
testInsertUpdate()
|
||||
{
|
||||
testcase("Insert/Update");
|
||||
Livecache<> c(clock_, journal_);
|
||||
|
||||
auto ep1 = Endpoint{beast::IP::randomEP(), 2};
|
||||
c.insert(ep1);
|
||||
BEAST_EXPECT(c.size() == 1);
|
||||
// third position list will contain the entry
|
||||
BEAST_EXPECT((c.hops.begin() + 2)->begin()->hops == 2);
|
||||
|
||||
auto ep2 = Endpoint{ep1.address, 4};
|
||||
// this will not change the entry has higher hops
|
||||
c.insert(ep2);
|
||||
BEAST_EXPECT(c.size() == 1);
|
||||
// still in third position list
|
||||
BEAST_EXPECT((c.hops.begin() + 2)->begin()->hops == 2);
|
||||
|
||||
auto ep3 = Endpoint{ep1.address, 2};
|
||||
// this will not change the entry has the same hops as existing
|
||||
c.insert(ep3);
|
||||
BEAST_EXPECT(c.size() == 1);
|
||||
// still in third position list
|
||||
BEAST_EXPECT((c.hops.begin() + 2)->begin()->hops == 2);
|
||||
|
||||
auto ep4 = Endpoint{ep1.address, 1};
|
||||
c.insert(ep4);
|
||||
BEAST_EXPECT(c.size() == 1);
|
||||
// now at second position list
|
||||
BEAST_EXPECT((c.hops.begin() + 1)->begin()->hops == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testExpire()
|
||||
{
|
||||
testcase("Expire");
|
||||
using namespace std::chrono_literals;
|
||||
Livecache<> c(clock_, journal_);
|
||||
|
||||
auto ep1 = Endpoint{beast::IP::randomEP(), 1};
|
||||
c.insert(ep1);
|
||||
BEAST_EXPECT(c.size() == 1);
|
||||
c.expire();
|
||||
BEAST_EXPECT(c.size() == 1);
|
||||
// verify that advancing to 1 sec before expiration
|
||||
// leaves our entry intact
|
||||
clock_.advance(Tuning::kLiveCacheSecondsToLive - 1s);
|
||||
c.expire();
|
||||
BEAST_EXPECT(c.size() == 1);
|
||||
// now advance to the point of expiration
|
||||
clock_.advance(1s);
|
||||
c.expire();
|
||||
BEAST_EXPECT(c.empty());
|
||||
}
|
||||
|
||||
void
|
||||
testHistogram()
|
||||
{
|
||||
testcase("Histogram");
|
||||
static constexpr auto kNumEps = 40;
|
||||
Livecache<> c(clock_, journal_);
|
||||
for (auto i = 0; i < kNumEps; ++i)
|
||||
add(beast::IP::randomEP(true), c, xrpl::randInt<std::uint32_t>());
|
||||
auto h = c.hops.histogram();
|
||||
if (!BEAST_EXPECT(!h.empty()))
|
||||
return;
|
||||
std::vector<std::string> v;
|
||||
boost::split(v, h, boost::algorithm::is_any_of(","));
|
||||
auto sum = 0;
|
||||
for (auto const& n : v)
|
||||
{
|
||||
auto val = boost::lexical_cast<int>(boost::trim_copy(n));
|
||||
sum += val;
|
||||
BEAST_EXPECT(val >= 0);
|
||||
}
|
||||
BEAST_EXPECT(sum == kNumEps);
|
||||
}
|
||||
|
||||
void
|
||||
testShuffle()
|
||||
{
|
||||
testcase("Shuffle");
|
||||
Livecache<> c(clock_, journal_);
|
||||
for (auto i = 0; i < 100; ++i)
|
||||
add(beast::IP::randomEP(true), c, xrpl::randInt(Tuning::kMaxHops + 1));
|
||||
|
||||
using at_hop = std::vector<xrpl::PeerFinder::Endpoint>;
|
||||
using all_hops = std::array<at_hop, 1 + Tuning::kMaxHops + 1>;
|
||||
|
||||
auto cmpEp = [](Endpoint const& a, Endpoint const& b) {
|
||||
return (b.hops < a.hops || (b.hops == a.hops && b.address < a.address));
|
||||
};
|
||||
all_hops before;
|
||||
all_hops beforeSorted;
|
||||
for (auto i = std::make_pair(0, c.hops.begin()); i.second != c.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], cmpEp);
|
||||
}
|
||||
|
||||
c.hops.shuffle();
|
||||
|
||||
all_hops after;
|
||||
all_hops afterSorted;
|
||||
for (auto i = std::make_pair(0, c.hops.begin()); i.second != c.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], cmpEp);
|
||||
}
|
||||
|
||||
// each hop bucket should contain the same items
|
||||
// before and after sort, albeit in different order
|
||||
bool allMatch = true;
|
||||
for (auto i = 0; i < before.size(); ++i)
|
||||
{
|
||||
BEAST_EXPECT(before[i].size() == after[i].size());
|
||||
allMatch = allMatch && (before[i] == after[i]);
|
||||
BEAST_EXPECT(beforeSorted[i] == afterSorted[i]);
|
||||
}
|
||||
BEAST_EXPECT(!allMatch);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testBasicInsert();
|
||||
testInsertUpdate();
|
||||
testExpire();
|
||||
testHistogram();
|
||||
testShuffle();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Livecache, peerfinder, xrpl);
|
||||
|
||||
} // namespace xrpl::PeerFinder
|
||||
@@ -1,789 +0,0 @@
|
||||
#include <test/unit_test/SuiteJournal.h>
|
||||
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/peerfinder/PeerfinderManager.h>
|
||||
#include <xrpld/peerfinder/detail/Counts.h>
|
||||
#include <xrpld/peerfinder/detail/Logic.h>
|
||||
#include <xrpld/peerfinder/detail/Store.h>
|
||||
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::PeerFinder {
|
||||
|
||||
class PeerFinder_test : public beast::unit_test::Suite
|
||||
{
|
||||
test::SuiteJournal journal_;
|
||||
|
||||
public:
|
||||
PeerFinder_test() : journal_("PeerFinder_test", *this)
|
||||
{
|
||||
}
|
||||
|
||||
struct TestStore : Store
|
||||
{
|
||||
std::size_t
|
||||
load(load_callback const& cb) override
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
save(std::vector<Entry> const&) override
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct TestChecker
|
||||
{
|
||||
void
|
||||
stop()
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
wait()
|
||||
{
|
||||
}
|
||||
|
||||
template <class Handler>
|
||||
void
|
||||
asyncConnect(beast::IP::Endpoint const& ep, Handler&& handler)
|
||||
{
|
||||
// NOLINTNEXTLINE(misc-const-correctness)
|
||||
boost::system::error_code ec;
|
||||
handler(ec);
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
testBackoff1()
|
||||
{
|
||||
auto const seconds = 10000;
|
||||
testcase("backoff 1");
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.1:5"));
|
||||
{
|
||||
Config c;
|
||||
c.autoConnect = false;
|
||||
c.listeningPort = 1024;
|
||||
logic.config(c);
|
||||
}
|
||||
std::size_t n = 0;
|
||||
for (std::size_t i = 0; i < seconds; ++i)
|
||||
{
|
||||
auto const list = logic.autoconnect();
|
||||
if (!list.empty())
|
||||
{
|
||||
BEAST_EXPECT(list.size() == 1);
|
||||
auto const [slot, _] = logic.newOutboundSlot(list.front());
|
||||
BEAST_EXPECT(
|
||||
logic.onConnected(slot, beast::IP::Endpoint::fromString("65.0.0.2:5")));
|
||||
logic.onClosed(slot);
|
||||
++n;
|
||||
}
|
||||
clock.advance(std::chrono::seconds(1));
|
||||
logic.oncePerSecond();
|
||||
}
|
||||
// Less than 20 attempts
|
||||
BEAST_EXPECT(n < 20);
|
||||
}
|
||||
|
||||
// with activate
|
||||
void
|
||||
testBackoff2()
|
||||
{
|
||||
auto const seconds = 10000;
|
||||
testcase("backoff 2");
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.1:5"));
|
||||
{
|
||||
Config c;
|
||||
c.autoConnect = false;
|
||||
c.listeningPort = 1024;
|
||||
logic.config(c);
|
||||
}
|
||||
|
||||
PublicKey const pk(randomKeyPair(KeyType::Secp256k1).first);
|
||||
std::size_t n = 0;
|
||||
|
||||
for (std::size_t i = 0; i < seconds; ++i)
|
||||
{
|
||||
auto const list = logic.autoconnect();
|
||||
if (!list.empty())
|
||||
{
|
||||
BEAST_EXPECT(list.size() == 1);
|
||||
auto const [slot, _] = logic.newOutboundSlot(list.front());
|
||||
if (!BEAST_EXPECT(
|
||||
logic.onConnected(slot, beast::IP::Endpoint::fromString("65.0.0.2:5"))))
|
||||
return;
|
||||
if (!BEAST_EXPECT(logic.activate(slot, pk, false) == PeerFinder::Result::Success))
|
||||
return;
|
||||
logic.onClosed(slot);
|
||||
++n;
|
||||
}
|
||||
clock.advance(std::chrono::seconds(1));
|
||||
logic.oncePerSecond();
|
||||
}
|
||||
// No more often than once per minute
|
||||
BEAST_EXPECT(n <= (seconds + 59) / 60);
|
||||
}
|
||||
|
||||
// test accepting an incoming slot for an already existing outgoing slot
|
||||
void
|
||||
testDuplicateOutIn()
|
||||
{
|
||||
testcase("duplicate out/in");
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
{
|
||||
Config c;
|
||||
c.autoConnect = false;
|
||||
c.listeningPort = 1024;
|
||||
c.ipLimit = 2;
|
||||
logic.config(c);
|
||||
}
|
||||
|
||||
auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5");
|
||||
auto const [slot1, r] = logic.newOutboundSlot(remote);
|
||||
BEAST_EXPECT(slot1 != nullptr);
|
||||
BEAST_EXPECT(r == Result::Success);
|
||||
BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1);
|
||||
|
||||
auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024");
|
||||
auto const [slot2, r2] = logic.newInboundSlot(local, remote);
|
||||
BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1);
|
||||
BEAST_EXPECT(r2 == Result::DuplicatePeer);
|
||||
|
||||
if (!BEAST_EXPECT(slot2 == nullptr))
|
||||
logic.onClosed(slot2);
|
||||
|
||||
logic.onClosed(slot1);
|
||||
}
|
||||
|
||||
// test establishing outgoing slot for an already existing incoming slot
|
||||
void
|
||||
testDuplicateInOut()
|
||||
{
|
||||
testcase("duplicate in/out");
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
{
|
||||
Config c;
|
||||
c.autoConnect = false;
|
||||
c.listeningPort = 1024;
|
||||
c.ipLimit = 2;
|
||||
logic.config(c);
|
||||
}
|
||||
|
||||
auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5");
|
||||
auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024");
|
||||
|
||||
auto const [slot1, r] = logic.newInboundSlot(local, remote);
|
||||
BEAST_EXPECT(slot1 != nullptr);
|
||||
BEAST_EXPECT(r == Result::Success);
|
||||
BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1);
|
||||
|
||||
auto const [slot2, r2] = logic.newOutboundSlot(remote);
|
||||
BEAST_EXPECT(r2 == Result::DuplicatePeer);
|
||||
BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1);
|
||||
if (!BEAST_EXPECT(slot2 == nullptr))
|
||||
logic.onClosed(slot2);
|
||||
logic.onClosed(slot1);
|
||||
}
|
||||
|
||||
void
|
||||
testPeerLimitExceeded()
|
||||
{
|
||||
testcase("peer limit exceeded");
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
{
|
||||
Config c;
|
||||
c.autoConnect = false;
|
||||
c.listeningPort = 1024;
|
||||
c.ipLimit = 2;
|
||||
logic.config(c);
|
||||
}
|
||||
|
||||
auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024");
|
||||
auto const [slot, r] =
|
||||
logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1025"));
|
||||
BEAST_EXPECT(slot != nullptr);
|
||||
BEAST_EXPECT(r == Result::Success);
|
||||
|
||||
auto const [slot1, r1] =
|
||||
logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1026"));
|
||||
BEAST_EXPECT(slot1 != nullptr);
|
||||
BEAST_EXPECT(r1 == Result::Success);
|
||||
|
||||
auto const [slot2, r2] =
|
||||
logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1027"));
|
||||
BEAST_EXPECT(r2 == Result::IpLimitExceeded);
|
||||
|
||||
if (!BEAST_EXPECT(slot2 == nullptr))
|
||||
logic.onClosed(slot2);
|
||||
logic.onClosed(slot1);
|
||||
logic.onClosed(slot);
|
||||
}
|
||||
|
||||
void
|
||||
testActivateDuplicatePeer()
|
||||
{
|
||||
testcase("test activate duplicate peer");
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
{
|
||||
Config c;
|
||||
c.autoConnect = false;
|
||||
c.listeningPort = 1024;
|
||||
c.ipLimit = 2;
|
||||
logic.config(c);
|
||||
}
|
||||
|
||||
auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024");
|
||||
|
||||
PublicKey const pk1(randomKeyPair(KeyType::Secp256k1).first);
|
||||
|
||||
auto const [slot, rSlot] =
|
||||
logic.newOutboundSlot(beast::IP::Endpoint::fromString("55.104.0.2:1025"));
|
||||
BEAST_EXPECT(slot != nullptr);
|
||||
BEAST_EXPECT(rSlot == Result::Success);
|
||||
|
||||
auto const [slot2, r2Slot] =
|
||||
logic.newOutboundSlot(beast::IP::Endpoint::fromString("55.104.0.2:1026"));
|
||||
BEAST_EXPECT(slot2 != nullptr);
|
||||
BEAST_EXPECT(r2Slot == Result::Success);
|
||||
|
||||
BEAST_EXPECT(logic.onConnected(slot, local));
|
||||
BEAST_EXPECT(logic.onConnected(slot2, local));
|
||||
|
||||
BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::Success);
|
||||
|
||||
// activating a different slot with the same node ID (pk) must fail
|
||||
BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::DuplicatePeer);
|
||||
|
||||
logic.onClosed(slot);
|
||||
|
||||
// accept the same key for a new slot after removing the old slot
|
||||
BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::Success);
|
||||
logic.onClosed(slot2);
|
||||
}
|
||||
|
||||
void
|
||||
testActivateInboundDisabled()
|
||||
{
|
||||
testcase("test activate inbound disabled");
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
{
|
||||
Config c;
|
||||
c.autoConnect = false;
|
||||
c.listeningPort = 1024;
|
||||
c.ipLimit = 2;
|
||||
logic.config(c);
|
||||
}
|
||||
|
||||
PublicKey const pk1(randomKeyPair(KeyType::Secp256k1).first);
|
||||
auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024");
|
||||
|
||||
auto const [slot, rSlot] =
|
||||
logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1025"));
|
||||
BEAST_EXPECT(slot != nullptr);
|
||||
BEAST_EXPECT(rSlot == Result::Success);
|
||||
|
||||
BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::InboundDisabled);
|
||||
|
||||
{
|
||||
Config c;
|
||||
c.autoConnect = false;
|
||||
c.listeningPort = 1024;
|
||||
c.ipLimit = 2;
|
||||
c.inPeers = 1;
|
||||
logic.config(c);
|
||||
}
|
||||
// new inbound slot must succeed when inbound connections are enabled
|
||||
BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::Success);
|
||||
|
||||
// creating a new inbound slot must succeed as IP Limit is not exceeded
|
||||
auto const [slot2, r2Slot] =
|
||||
logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1026"));
|
||||
BEAST_EXPECT(slot2 != nullptr);
|
||||
BEAST_EXPECT(r2Slot == Result::Success);
|
||||
|
||||
PublicKey const pk2(randomKeyPair(KeyType::Secp256k1).first);
|
||||
|
||||
// an inbound slot exceeding inPeers limit must fail
|
||||
BEAST_EXPECT(logic.activate(slot2, pk2, false) == Result::Full);
|
||||
|
||||
logic.onClosed(slot2);
|
||||
logic.onClosed(slot);
|
||||
}
|
||||
|
||||
void
|
||||
testAddFixedPeerNoPort()
|
||||
{
|
||||
testcase("test addFixedPeer no port");
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
try
|
||||
{
|
||||
logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.2"));
|
||||
fail("invalid endpoint successfully added");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
pass();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIsValidAddress()
|
||||
{
|
||||
testcase("is_valid_address");
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
|
||||
auto const pass = [&](std::string const& s) {
|
||||
BEAST_EXPECT(logic.isValidAddress(beast::IP::Endpoint::fromString(s)));
|
||||
};
|
||||
auto const fail = [&](std::string const& s) {
|
||||
BEAST_EXPECT(!logic.isValidAddress(beast::IP::Endpoint::fromString(s)));
|
||||
};
|
||||
|
||||
// Invalid: port 0
|
||||
fail("65.0.0.1:0");
|
||||
|
||||
// --- IPv4 ranges ---
|
||||
// For each range: 1 before (pass), first (fail), last (fail),
|
||||
// 1 after (pass)
|
||||
|
||||
// 0.0.0.0/8 - "This network"
|
||||
// No "before" - nothing before 0.0.0.0
|
||||
fail("0.0.0.0:8080");
|
||||
fail("0.255.255.255:8080");
|
||||
pass("1.0.0.0:8080");
|
||||
|
||||
// 10.0.0.0/8 - Private (RFC 1918)
|
||||
pass("9.255.255.255:8080");
|
||||
fail("10.0.0.0:8080");
|
||||
fail("10.255.255.255:8080");
|
||||
pass("11.0.0.0:8080");
|
||||
|
||||
// 100.64.0.0/10 - Shared Address Space / CGNAT (RFC 6598)
|
||||
pass("100.63.255.255:8080");
|
||||
fail("100.64.0.0:8080");
|
||||
fail("100.127.255.255:8080");
|
||||
pass("100.128.0.0:8080");
|
||||
|
||||
// 127.0.0.0/8 - Loopback
|
||||
pass("126.255.255.255:8080");
|
||||
fail("127.0.0.0:8080");
|
||||
fail("127.255.255.255:8080");
|
||||
pass("128.0.0.0:8080");
|
||||
|
||||
// 169.254.0.0/16 - Link-local
|
||||
pass("169.253.255.255:8080");
|
||||
fail("169.254.0.0:8080");
|
||||
fail("169.254.255.255:8080");
|
||||
pass("169.255.0.0:8080");
|
||||
|
||||
// 172.16.0.0/12 - Private (RFC 1918)
|
||||
pass("172.15.255.255:8080");
|
||||
fail("172.16.0.0:8080");
|
||||
fail("172.31.255.255:8080");
|
||||
pass("172.32.0.0:8080");
|
||||
|
||||
// 192.0.0.0/24 - IETF Protocol Assignments (RFC 6890)
|
||||
pass("191.255.255.255:8080");
|
||||
fail("192.0.0.0:8080");
|
||||
fail("192.0.0.255:8080");
|
||||
pass("192.0.1.0:8080");
|
||||
|
||||
// 192.0.2.0/24 - TEST-NET-1 (RFC 5737)
|
||||
pass("192.0.1.255:8080");
|
||||
fail("192.0.2.0:8080");
|
||||
fail("192.0.2.255:8080");
|
||||
pass("192.0.3.0:8080");
|
||||
|
||||
// 192.88.99.0/24 - 6to4 Relay Anycast (RFC 7526)
|
||||
pass("192.88.98.255:8080");
|
||||
fail("192.88.99.0:8080");
|
||||
fail("192.88.99.255:8080");
|
||||
pass("192.88.100.0:8080");
|
||||
|
||||
// 192.168.0.0/16 - Private (RFC 1918)
|
||||
pass("192.167.255.255:8080");
|
||||
fail("192.168.0.0:8080");
|
||||
fail("192.168.255.255:8080");
|
||||
pass("192.169.0.0:8080");
|
||||
|
||||
// 198.18.0.0/15 - Benchmarking (RFC 2544)
|
||||
pass("198.17.255.255:8080");
|
||||
fail("198.18.0.0:8080");
|
||||
fail("198.19.255.255:8080");
|
||||
pass("198.20.0.0:8080");
|
||||
|
||||
// 198.51.100.0/24 - TEST-NET-2 (RFC 5737)
|
||||
pass("198.51.99.255:8080");
|
||||
fail("198.51.100.0:8080");
|
||||
fail("198.51.100.255:8080");
|
||||
pass("198.51.101.0:8080");
|
||||
|
||||
// 203.0.113.0/24 - TEST-NET-3 (RFC 5737)
|
||||
pass("203.0.112.255:8080");
|
||||
fail("203.0.113.0:8080");
|
||||
fail("203.0.113.255:8080");
|
||||
pass("203.0.114.0:8080");
|
||||
|
||||
// 224.0.0.0/4 - Multicast
|
||||
pass("223.255.255.255:8080");
|
||||
fail("224.0.0.0:8080");
|
||||
fail("239.255.255.255:8080");
|
||||
// 240.0.0.0 (after multicast) is also blocked (reserved)
|
||||
|
||||
// 240.0.0.0/4 - Reserved (RFC 1112)
|
||||
// 239.255.255.255 (before reserved) is also blocked (multicast)
|
||||
fail("240.0.0.0:8080");
|
||||
fail("255.255.255.255:8080");
|
||||
|
||||
// --- IPv6 ranges ---
|
||||
|
||||
// ::1 - Loopback (single address)
|
||||
fail("[::1]:8080");
|
||||
|
||||
// :: - Unspecified (single address)
|
||||
fail("[::]:8080");
|
||||
|
||||
// fc00::/7 - Unique Local Address (ULA)
|
||||
pass("[fb00::1]:8080");
|
||||
fail("[fc00::1]:8080");
|
||||
fail("[fdff::1]:8080");
|
||||
pass("[fe00::1]:8080");
|
||||
|
||||
// fe80::/10 - Link-local
|
||||
pass("[fe7f::1]:8080");
|
||||
fail("[fe80::1]:8080");
|
||||
fail("[febf::1]:8080");
|
||||
pass("[fec0::1]:8080");
|
||||
|
||||
// ff00::/8 - Multicast
|
||||
pass("[feff::1]:8080");
|
||||
fail("[ff00::1]:8080");
|
||||
fail("[ffff::1]:8080");
|
||||
// No "after" - ffff:... is the highest IPv6 range
|
||||
|
||||
// 100::/64 - Discard prefix (RFC 6666)
|
||||
pass("[ff::1]:8080");
|
||||
fail("[100::]:8080");
|
||||
fail("[100::ffff:ffff:ffff:ffff]:8080");
|
||||
pass("[100:0:0:1::1]:8080");
|
||||
|
||||
// 2001::/32 - IETF Protocol Assignments / Teredo (RFC 4380)
|
||||
pass("[2000:ffff::1]:8080");
|
||||
fail("[2001::]:8080");
|
||||
fail("[2001:0:ffff::1]:8080");
|
||||
pass("[2001:1::1]:8080");
|
||||
|
||||
// 2001:20::/28 - ORCHIDv2 (RFC 7343)
|
||||
pass("[2001:1f::1]:8080");
|
||||
fail("[2001:20::1]:8080");
|
||||
fail("[2001:2f::1]:8080");
|
||||
pass("[2001:30::1]:8080");
|
||||
|
||||
// 2001:db8::/32 - Documentation (RFC 3849)
|
||||
pass("[2001:db7::1]:8080");
|
||||
fail("[2001:db8::1]:8080");
|
||||
fail("[2001:db8:ffff::1]:8080");
|
||||
pass("[2001:db9::1]:8080");
|
||||
|
||||
// 2002::/16 - 6to4 (RFC 3056, deprecated)
|
||||
pass("[2001:ffff::1]:8080");
|
||||
fail("[2002::1]:8080");
|
||||
fail("[2002:ffff::1]:8080");
|
||||
pass("[2003::1]:8080");
|
||||
|
||||
// --- IPv6 v4-mapped (delegates to IPv4 checks) ---
|
||||
fail("[::ffff:10.0.0.1]:8080");
|
||||
fail("[::ffff:100.64.0.1]:8080");
|
||||
fail("[::ffff:169.254.1.1]:8080");
|
||||
fail("[::ffff:192.0.2.1]:8080");
|
||||
fail("[::ffff:198.18.0.1]:8080");
|
||||
fail("[::ffff:224.0.0.1]:8080");
|
||||
fail("[::ffff:240.0.0.1]:8080");
|
||||
|
||||
// --- Valid public addresses ---
|
||||
pass("8.8.8.8:443");
|
||||
pass("65.0.0.1:8080");
|
||||
pass("[2001:4860:4860::8888]:8080");
|
||||
pass("[2606:4700:4700::1111]:8080");
|
||||
}
|
||||
|
||||
void
|
||||
testVerifyEndpoints()
|
||||
{
|
||||
// Helper that sets up a Logic instance, creates and activates a slot,
|
||||
// then calls on_endpoints with the given list and returns the
|
||||
// livecache size afterwards.
|
||||
auto run = [&](bool verifyEndpoints, Endpoints eps) -> std::size_t {
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
{
|
||||
Config c;
|
||||
c.autoConnect = false;
|
||||
c.listeningPort = 1024;
|
||||
c.ipLimit = 2;
|
||||
c.verifyEndpoints = verifyEndpoints;
|
||||
logic.config(c);
|
||||
}
|
||||
|
||||
auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5");
|
||||
auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024");
|
||||
|
||||
auto const [slot, r] = logic.newOutboundSlot(remote);
|
||||
BEAST_EXPECT(slot != nullptr);
|
||||
BEAST_EXPECT(r == Result::Success);
|
||||
BEAST_EXPECT(logic.onConnected(slot, local));
|
||||
|
||||
PublicKey const pk(randomKeyPair(KeyType::Secp256k1).first);
|
||||
BEAST_EXPECT(logic.activate(slot, pk, false) == Result::Success);
|
||||
|
||||
logic.onEndpoints(slot, std::move(eps));
|
||||
|
||||
auto const size = logic.livecache.size();
|
||||
logic.onClosed(slot);
|
||||
return size;
|
||||
};
|
||||
|
||||
{
|
||||
testcase("verify_endpoints enabled");
|
||||
|
||||
// Valid public addresses
|
||||
Endpoints eps;
|
||||
eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.1:5"), 1);
|
||||
eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.2:6"), 1);
|
||||
// Invalid: private address
|
||||
eps.emplace_back(beast::IP::Endpoint::fromString("10.0.0.1:5"), 1);
|
||||
// Invalid: port 0
|
||||
eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.3:0"), 1);
|
||||
|
||||
// With verification enabled, only the 2 valid endpoints survive
|
||||
BEAST_EXPECT(run(true, eps) == 2);
|
||||
}
|
||||
{
|
||||
testcase("verify_endpoints disabled");
|
||||
|
||||
Endpoints eps;
|
||||
eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.1:5"), 1);
|
||||
eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.2:6"), 1);
|
||||
// Private address — kept when verification is off
|
||||
eps.emplace_back(beast::IP::Endpoint::fromString("10.0.0.1:5"), 1);
|
||||
// Port 0 — kept when verification is off
|
||||
eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.3:0"), 1);
|
||||
|
||||
// Without verification, all 4 endpoints survive
|
||||
BEAST_EXPECT(run(false, eps) == 4);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testOnConnectedSelfConnection()
|
||||
{
|
||||
testcase("test onConnected self connection");
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
|
||||
auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1234");
|
||||
auto const [slot, r] = logic.newOutboundSlot(local);
|
||||
BEAST_EXPECT(slot != nullptr);
|
||||
BEAST_EXPECT(r == Result::Success);
|
||||
|
||||
// Must fail when a slot is to our own IP address
|
||||
BEAST_EXPECT(!logic.onConnected(slot, local));
|
||||
logic.onClosed(slot);
|
||||
}
|
||||
|
||||
void
|
||||
testConfig()
|
||||
{
|
||||
// if peers_max is configured then peers_in_max and peers_out_max
|
||||
// are ignored
|
||||
auto run = [&](std::string const& test,
|
||||
std::optional<std::uint16_t> maxPeers,
|
||||
std::optional<std::uint16_t> maxIn,
|
||||
std::optional<std::uint16_t> maxOut,
|
||||
std::uint16_t port,
|
||||
std::uint16_t expectOut,
|
||||
std::uint16_t expectIn,
|
||||
std::uint16_t expectIpLimit) {
|
||||
xrpl::Config c;
|
||||
|
||||
testcase(test);
|
||||
|
||||
std::string toLoad;
|
||||
int max = 0;
|
||||
if (maxPeers)
|
||||
{
|
||||
max = maxPeers.value();
|
||||
toLoad += "[peers_max]\n" + std::to_string(max) + "\n" + "[peers_in_max]\n" +
|
||||
std::to_string(maxIn.value_or(0)) + "\n" + "[peers_out_max]\n" +
|
||||
std::to_string(maxOut.value_or(0)) + "\n";
|
||||
}
|
||||
else if (maxIn && maxOut)
|
||||
{
|
||||
toLoad += "[peers_in_max]\n" + std::to_string(*maxIn) + "\n" + "[peers_out_max]\n" +
|
||||
std::to_string(*maxOut) + "\n";
|
||||
}
|
||||
|
||||
c.loadFromString(toLoad);
|
||||
BEAST_EXPECT(
|
||||
(c.peersMax == max && c.peersInMax == 0 && c.peersOutMax == 0) ||
|
||||
(c.peersInMax == *maxIn && c.peersOutMax == *maxOut));
|
||||
|
||||
Config const config = Config::makeConfig(c, port, false, 0, true);
|
||||
|
||||
Counts counts;
|
||||
counts.onConfig(config);
|
||||
BEAST_EXPECT(
|
||||
counts.outMax() == expectOut && counts.inMax() == expectIn &&
|
||||
config.ipLimit == expectIpLimit);
|
||||
|
||||
TestStore store;
|
||||
TestChecker checker;
|
||||
TestStopwatch clock;
|
||||
Logic<TestChecker> logic(clock, store, checker, journal_);
|
||||
logic.config(config);
|
||||
|
||||
BEAST_EXPECT(logic.config() == config);
|
||||
};
|
||||
|
||||
// if max_peers == 0 => maxPeers = 21,
|
||||
// else if max_peers < 10 => maxPeers = 10 else maxPeers =
|
||||
// max_peers
|
||||
// expectOut => if legacy => max(0.15 * maxPeers, 10),
|
||||
// if legacy && !wantIncoming => maxPeers else max_out_peers
|
||||
// expectIn => if legacy && wantIncoming => maxPeers - outPeers
|
||||
// else if !wantIncoming => 0 else max_in_peers
|
||||
// ipLimit => if expectIn <= 21 => 2 else 2 + min(5, expectIn/21)
|
||||
// ipLimit = max(1, min(ipLimit, expectIn/2))
|
||||
|
||||
// legacy test with max_peers
|
||||
run("legacy no config", {}, {}, {}, 4000, 10, 11, 2);
|
||||
run("legacy max_peers 0", 0, 100, 10, 4000, 10, 11, 2);
|
||||
run("legacy max_peers 5", 5, 100, 10, 4000, 10, 0, 1);
|
||||
run("legacy max_peers 20", 20, 100, 10, 4000, 10, 10, 2);
|
||||
run("legacy max_peers 100", 100, 100, 10, 4000, 15, 85, 6);
|
||||
run("legacy max_peers 20, private", 20, 100, 10, 0, 20, 0, 1);
|
||||
|
||||
// test with max_in_peers and max_out_peers
|
||||
run("new in 100/out 10", {}, 100, 10, 4000, 10, 100, 6);
|
||||
run("new in 0/out 10", {}, 0, 10, 4000, 10, 0, 1);
|
||||
run("new in 100/out 10, private", {}, 100, 10, 0, 10, 0, 6);
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidConfig()
|
||||
{
|
||||
testcase("invalid config");
|
||||
|
||||
auto run = [&](std::string const& toLoad) {
|
||||
xrpl::Config c;
|
||||
try
|
||||
{
|
||||
c.loadFromString(toLoad);
|
||||
fail();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
pass();
|
||||
}
|
||||
};
|
||||
run(R"xrpldConfig(
|
||||
[peers_in_max]
|
||||
100
|
||||
)xrpldConfig");
|
||||
run(R"xrpldConfig(
|
||||
[peers_out_max]
|
||||
100
|
||||
)xrpldConfig");
|
||||
run(R"xrpldConfig(
|
||||
[peers_in_max]
|
||||
100
|
||||
[peers_out_max]
|
||||
5
|
||||
)xrpldConfig");
|
||||
run(R"xrpldConfig(
|
||||
[peers_in_max]
|
||||
1001
|
||||
[peers_out_max]
|
||||
10
|
||||
)xrpldConfig");
|
||||
run(R"xrpldConfig(
|
||||
[peers_in_max]
|
||||
10
|
||||
[peers_out_max]
|
||||
1001
|
||||
)xrpldConfig");
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testBackoff1();
|
||||
testBackoff2();
|
||||
testDuplicateOutIn();
|
||||
testDuplicateInOut();
|
||||
testConfig();
|
||||
testInvalidConfig();
|
||||
testPeerLimitExceeded();
|
||||
testActivateDuplicatePeer();
|
||||
testActivateInboundDisabled();
|
||||
testAddFixedPeerNoPort();
|
||||
testOnConnectedSelfConnection();
|
||||
testIsValidAddress();
|
||||
testVerifyEndpoints();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(PeerFinder, peerfinder, xrpl);
|
||||
|
||||
} // namespace xrpl::PeerFinder
|
||||
Reference in New Issue
Block a user