diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index d73f5dc2c35..c271c369b50 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -4475,14 +4475,26 @@ True True + + True + True + True True + + True + True + True True + + True + True + True True @@ -4531,19 +4543,61 @@ True True - + + + + + + + + True + True + + + + + + + True + True + + True True - + + True + True + + + + + + + + + + + + True + True + + + + + + + + + - + True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 326781e37c7..a21087161cf 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -5238,12 +5238,21 @@ test\conditions + + test\consensus + test\consensus + + test\consensus + test\consensus + + test\consensus + test\consensus @@ -5283,22 +5292,73 @@ test\csf - + + test\csf + + + test\csf + + + test\csf + + + test\csf + + + test\csf + + + test\csf + + + test\csf + + test\csf\impl - + + test\csf\impl + + test\csf test\csf + + test\csf + + + test\csf + + + test\csf + + + test\csf + + + test\csf + test\csf + + test\csf + + + test\csf + + + test\csf + + + test\csf + test\csf - + test\csf diff --git a/docs/consensus.qbk b/docs/consensus.qbk index 832413201cf..70491a6f700 100644 --- a/docs/consensus.qbk +++ b/docs/consensus.qbk @@ -458,10 +458,12 @@ struct Ledger { using ID = ...; + using Seq = //std::uint32_t?...; + ID const & id() const; // Sequence number that is 1 more than the parent ledger's seq() - std::size_t seq() const; + Seq seq() const; // Whether the ledger's close time was a non-trivial consensus result bool closeAgree() const; @@ -633,14 +635,14 @@ struct Adaptor // Propose the position to peers. void propose(ConsensusProposal<...> const & pos); - // Relay a received peer proposal on to other peer's. - void relay(PeerPosition_t const & pos); + // Share a received peer proposal with other peers. + void share(PeerPosition_t const & pos); - // Relay a disputed transaction to peers - void relay(TxSet::Tx const & tx); + // Share a disputed transaction with peers + void share(TxSet::Tx const & tx); - // Realy given transaction set with peers - void relay(TxSet const &s); + // Share given transaction set with peers + void share(TxSet const &s); //... implementation specific }; @@ -649,9 +651,9 @@ struct Adaptor The implementing class hides many details of the peer communication model from the generic code. -* The =relay= member functions are responsible for sharing the given type with a +* The =share= member functions are responsible for sharing the given type with a node's peers, but are agnostic to the mechanism. Ideally, messages are delivered - faster than =LEDGER_GRANULARITY=. + faster than =LEDGER_GRANULARITY=. * The generic code does not specify how transactions are submitted by clients, propagated through the network or stored in the open ledger. Indeed, the open ledger is only conceptual from the perspective of the generic code---the diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index 1c74d4134cf..e858a67544c 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -126,7 +126,7 @@ RCLConsensus::Adaptor::acquireLedger(LedgerHash const& ledger) void -RCLConsensus::Adaptor::relay(RCLCxPeerPos const& peerPos) +RCLConsensus::Adaptor::share(RCLCxPeerPos const& peerPos) { protocol::TMProposeSet prop; @@ -150,7 +150,7 @@ RCLConsensus::Adaptor::relay(RCLCxPeerPos const& peerPos) } void -RCLConsensus::Adaptor::relay(RCLCxTx const& tx) +RCLConsensus::Adaptor::share(RCLCxTx const& tx) { // If we didn't relay this transaction recently, relay it to all peers if (app_.getHashRouter().shouldRelay(tx.id())) @@ -204,7 +204,7 @@ RCLConsensus::Adaptor::propose(RCLCxPeerPos::Proposal const& proposal) } void -RCLConsensus::Adaptor::relay(RCLTxSet const& set) +RCLConsensus::Adaptor::share(RCLTxSet const& set) { inboundTransactions_.giveSet(set.id(), set.map_, false); } @@ -254,19 +254,7 @@ RCLConsensus::Adaptor::getPrevLedger( app_.getValidations().currentTrustedDistribution( ledgerID, parentID, ledgerMaster_.getValidLedgerIndex()); - uint256 netLgr = ledgerID; - int netLgrCount = 0; - for (auto const & it : ledgerCounts) - { - // Switch to ledger supported by more peers - // Or stick with ours on a tie - if ((it.second > netLgrCount) || - ((it.second == netLgrCount) && (it.first == ledgerID))) - { - netLgr = it.first; - netLgrCount = it.second; - } - } + uint256 netLgr = getPreferredLedger(ledgerID, ledgerCounts); if (netLgr != ledgerID) { diff --git a/src/ripple/app/consensus/RCLConsensus.h b/src/ripple/app/consensus/RCLConsensus.h index 5522b8d4e87..2211beb3fea 100644 --- a/src/ripple/app/consensus/RCLConsensus.h +++ b/src/ripple/app/consensus/RCLConsensus.h @@ -158,21 +158,21 @@ class RCLConsensus boost::optional acquireLedger(LedgerHash const& ledger); - /** Relay the given proposal to all peers + /** Share the given proposal with all peers - @param peerPos The peer position to relay. + @param peerPos The peer position to share. */ void - relay(RCLCxPeerPos const& peerPos); + share(RCLCxPeerPos const& peerPos); - /** Relay disputed transacction to peers. + /** Share disputed transaction to peers. - Only relay if the provided transaction hasn't been shared recently. + Only share if the provided transaction hasn't been shared recently. - @param tx The disputed transaction to relay. + @param tx The disputed transaction to share. */ void - relay(RCLCxTx const& tx); + share(RCLCxTx const& tx); /** Acquire the transaction set associated with a proposal. @@ -215,12 +215,12 @@ class RCLConsensus void propose(RCLCxPeerPos::Proposal const& proposal); - /** Relay the given tx set to peers. + /** Share the given tx set to peers. @param set The TxSet to share. */ void - relay(RCLTxSet const& set); + share(RCLTxSet const& set); /** Get the ID of the previous ledger/last closed ledger(LCL) on the network diff --git a/src/ripple/app/consensus/RCLCxLedger.h b/src/ripple/app/consensus/RCLCxLedger.h index 29841d47218..58bc7a4d484 100644 --- a/src/ripple/app/consensus/RCLCxLedger.h +++ b/src/ripple/app/consensus/RCLCxLedger.h @@ -37,6 +37,8 @@ class RCLCxLedger public: //! Unique identifier of a ledger using ID = LedgerHash; + //! Sequence number of a ledger + using Seq = LedgerIndex; /** Default constructor @@ -55,28 +57,28 @@ class RCLCxLedger } //! Sequence number of the ledger. - auto const& + Seq const& seq() const { return ledger_->info().seq; } //! Unique identifier (hash) of this ledger. - auto const& + ID const& id() const { return ledger_->info().hash; } //! Unique identifier (hash) of this ledger's parent. - auto const& + ID const& parentID() const { return ledger_->info().parentHash; } //! Resolution used when calculating this ledger's close time. - auto + NetClock::duration closeTimeResolution() const { return ledger_->info().closeTimeResolution; @@ -90,14 +92,14 @@ class RCLCxLedger } //! The close time of this ledger - auto + NetClock::time_point closeTime() const { return ledger_->info().closeTime; } //! The close time of this ledger's parent. - auto + NetClock::time_point parentCloseTime() const { return ledger_->info().parentCloseTime; diff --git a/src/ripple/basics/tagged_integer.h b/src/ripple/basics/tagged_integer.h index b713ecc491c..a8b60fc515a 100644 --- a/src/ripple/basics/tagged_integer.h +++ b/src/ripple/basics/tagged_integer.h @@ -41,9 +41,18 @@ namespace ripple { allowed arithmetic operations. */ template -class tagged_integer : boost::operators> - , boost::shiftable> +class tagged_integer + : boost::totally_ordered< + tagged_integer, + boost::integer_arithmetic< + tagged_integer, + boost::bitwise< + tagged_integer, + boost::unit_steppable< + tagged_integer, + boost::shiftable>>>>> { + private: Int m_value; @@ -63,6 +72,9 @@ class tagged_integer : boost::operators> tagged_integer(OtherInt value) noexcept : m_value(value) { + static_assert( + sizeof(tagged_integer) == sizeof(Int), + "tagged_integer is adding padding"); } bool diff --git a/src/ripple/consensus/Consensus.h b/src/ripple/consensus/Consensus.h index 572430f13dc..7b72e074d5f 100644 --- a/src/ripple/consensus/Consensus.h +++ b/src/ripple/consensus/Consensus.h @@ -176,10 +176,11 @@ checkConsensus( struct Ledger { using ID = ...; + using Seq = ...; // Unique identifier of ledgerr ID const id() const; - auto seq() const; + Seq seq() const; auto closeTimeResolution() const; auto closeAgree() const; auto closeTime() const; @@ -257,14 +258,14 @@ checkConsensus( // Propose the position to peers. void propose(ConsensusProposal<...> const & pos); - // Relay a received peer proposal on to other peer's. - void relay(PeerPosition_t const & prop); + // Share a received peer proposal with other peer's. + void share(PeerPosition_t const & prop); - // Relay a disputed transaction to peers - void relay(Txn const & tx); + // Share a disputed transaction with peers + void share(Txn const & tx); // Share given transaction set with peers - void relay(TxSet const &s); + void share(TxSet const &s); // Consensus timing parameters and constants ConsensusParms const & @@ -642,7 +643,7 @@ Consensus::startRoundInternal( closeResolution_ = getNextLedgerTimeResolution( previousLedger_.closeTimeResolution(), previousLedger_.closeAgree(), - previousLedger_.seq() + 1); + previousLedger_.seq() + typename Ledger_t::Seq{1}); playbackProposals(); if (currPeerPositions_.size() > (prevProposers_ / 2)) @@ -878,7 +879,7 @@ Consensus::getJson(bool full) const if (mode_.get() != ConsensusMode::wrongLedger) { ret["synched"] = true; - ret["ledger_seq"] = previousLedger_.seq() + 1; + ret["ledger_seq"] = static_cast(previousLedger_.seq())+ 1; ret["close_granularity"] = static_cast(closeResolution_.count()); } else @@ -1038,7 +1039,7 @@ Consensus::playbackProposals() if (pos.proposal().prevLedger() == prevLedgerID_) { if (peerProposalInternal(now_, pos)) - adaptor_.relay(pos); + adaptor_.share(pos); } } } @@ -1158,7 +1159,7 @@ Consensus::closeLedger() // Share the newly created transaction set if we haven't already // received it from a peer if (acquired_.emplace(result_->set.id(), result_->set).second) - adaptor_.relay(result_->set); + adaptor_.share(result_->set); if (mode_.get() == ConsensusMode::proposing) adaptor_.propose(result_->position); @@ -1264,7 +1265,7 @@ Consensus::updateOurPositions() } if (mutableSet) - ourNewSet.emplace(*mutableSet); + ourNewSet.emplace(std::move(*mutableSet)); } NetClock::time_point consensusCloseTime = {}; @@ -1310,7 +1311,8 @@ Consensus::updateOurPositions() for (auto const& it : closeTimeVotes) { JLOG(j_.debug()) - << "CCTime: seq " << previousLedger_.seq() + 1 << ": " + << "CCTime: seq " + << static_cast(previousLedger_.seq()) + 1 << ": " << it.first.time_since_epoch().count() << " has " << it.second << ", " << threshVote << " required"; @@ -1361,7 +1363,7 @@ Consensus::updateOurPositions() if (acquired_.emplace(newID, result_->set).second) { if (!result_->position.isBowOut()) - adaptor_.relay(result_->set); + adaptor_.share(result_->set); for (auto const& it : currPeerPositions_) { @@ -1493,7 +1495,8 @@ Consensus::createDisputes(TxSet_t const& o) JLOG(j_.debug()) << "Transaction " << txID << " is disputed"; - typename Result::Dispute_t dtx{tx, result_->set.exists(txID), j_}; + typename Result::Dispute_t dtx{tx, result_->set.exists(txID), + std::max(prevProposers_, currPeerPositions_.size()), j_}; // Update all of the available peer's votes on the disputed transaction for (auto const& pit : currPeerPositions_) @@ -1503,7 +1506,7 @@ Consensus::createDisputes(TxSet_t const& o) if (cit != acquired_.end()) dtx.setVote(pit.first, cit->second.exists(txID)); } - adaptor_.relay(dtx.tx()); + adaptor_.share(dtx.tx()); result_->disputes.emplace(txID, std::move(dtx)); } diff --git a/src/ripple/consensus/DisputedTx.h b/src/ripple/consensus/DisputedTx.h index c0ef35f9568..ed0360ebd0c 100644 --- a/src/ripple/consensus/DisputedTx.h +++ b/src/ripple/consensus/DisputedTx.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_APP_CONSENSUS_IMPL_DISPUTEDTX_H_INCLUDED #define RIPPLE_APP_CONSENSUS_IMPL_DISPUTEDTX_H_INCLUDED +#include #include #include #include @@ -48,17 +49,19 @@ template class DisputedTx { using TxID_t = typename Tx_t::ID; - + using Map_t = boost::container::flat_map; public: /** Constructor @param tx The transaction under dispute @param ourVote Our vote on whether tx should be included + @param numPeers Anticipated number of peer votes @param j Journal for debugging */ - DisputedTx(Tx_t const& tx, bool ourVote, beast::Journal j) + DisputedTx(Tx_t const& tx, bool ourVote, std::size_t numPeers, beast::Journal j) : yays_(0), nays_(0), ourVote_(ourVote), tx_(tx), j_(j) { + votes_.reserve(numPeers); } //! The unique id/hash of the disputed transaction. @@ -127,9 +130,8 @@ class DisputedTx int nays_; //< Number of no votes bool ourVote_; //< Our vote (true is yes) Tx_t tx_; //< Transaction under dispute - - hash_map votes_; //< Votes of our peers - beast::Journal j_; //< Debug journal + Map_t votes_; //< Map from NodeID to vote + beast::Journal j_; }; // Track a peer's yes/no vote on a particular disputed tx_ diff --git a/src/ripple/consensus/LedgerTiming.h b/src/ripple/consensus/LedgerTiming.h index 08552c347d1..648c3a41e36 100644 --- a/src/ripple/consensus/LedgerTiming.h +++ b/src/ripple/consensus/LedgerTiming.h @@ -46,7 +46,6 @@ auto constexpr increaseLedgerTimeResolutionEvery = 8; //! How often we decrease the close time resolution (in numbers of ledgers) auto constexpr decreaseLedgerTimeResolutionEvery = 1; - /** Calculates the close time resolution for the specified ledger. The Ripple protocol uses binning to represent time intervals using only one @@ -62,15 +61,22 @@ auto constexpr decreaseLedgerTimeResolutionEvery = 1; @pre previousResolution must be a valid bin from @ref ledgerPossibleTimeResolutions + + @tparam Rep Type representing number of ticks in std::chrono::duration + @tparam Period An std::ratio representing tick period in + std::chrono::duration + @tparam Seq Unsigned integer-like type corresponding to the ledger sequence + number. It should be comparable to 0 and support modular + division. Built-in and tagged_integers are supported. */ -template -duration +template +std::chrono::duration getNextLedgerTimeResolution( - duration previousResolution, + std::chrono::duration previousResolution, bool previousAgree, - std::uint32_t ledgerSeq) + Seq ledgerSeq) { - assert(ledgerSeq); + assert(ledgerSeq != Seq{0}); using namespace std::chrono; // Find the current resolution: @@ -86,7 +92,8 @@ getNextLedgerTimeResolution( // If we did not previously agree, we try to decrease the resolution to // improve the chance that we will agree now. - if (!previousAgree && ledgerSeq % decreaseLedgerTimeResolutionEvery == 0) + if (!previousAgree && + (ledgerSeq % Seq{decreaseLedgerTimeResolutionEvery} == Seq{0})) { if (++iter != std::end(ledgerPossibleTimeResolutions)) return *iter; @@ -94,7 +101,8 @@ getNextLedgerTimeResolution( // If we previously agreed, we try to increase the resolution to determine // if we can continue to agree. - if (previousAgree && ledgerSeq % increaseLedgerTimeResolutionEvery == 0) + if (previousAgree && + (ledgerSeq % Seq{increaseLedgerTimeResolutionEvery} == Seq{0})) { if (iter-- != std::begin(ledgerPossibleTimeResolutions)) return *iter; @@ -110,12 +118,13 @@ getNextLedgerTimeResolution( @return @b closeTime rounded to the nearest multiple of @b closeResolution. Rounds up if @b closeTime is midway between multiples of @b closeResolution. */ -template -time_point +template +std::chrono::time_point roundCloseTime( - time_point closeTime, - typename time_point::duration closeResolution) + std::chrono::time_point closeTime, + std::chrono::duration closeResolution) { + using time_point = decltype(closeTime); if (closeTime == time_point{}) return closeTime; @@ -132,14 +141,15 @@ roundCloseTime( @param resolution The current close time resolution @param priorCloseTime The close time of the prior ledger */ -template -time_point +template +std::chrono::time_point effCloseTime( - time_point closeTime, - typename time_point::duration const resolution, - time_point priorCloseTime) + std::chrono::time_point closeTime, + std::chrono::duration resolution, + std::chrono::time_point priorCloseTime) { using namespace std::chrono_literals; + using time_point = decltype(closeTime); if (closeTime == time_point{}) return closeTime; diff --git a/src/ripple/consensus/Validations.h b/src/ripple/consensus/Validations.h index cd1688bf44f..ad80698e5ab 100644 --- a/src/ripple/consensus/Validations.h +++ b/src/ripple/consensus/Validations.h @@ -26,7 +26,6 @@ #include #include #include -#include #include #include #include @@ -74,6 +73,7 @@ struct ValidationParms std::chrono::seconds validationSET_EXPIRES = std::chrono::minutes{10}; }; + /** Whether a validation is still current Determines whether a validation can still be considered the current @@ -103,6 +103,37 @@ isCurrent( (seenTime < (now + p.validationCURRENT_LOCAL))); } +/** Determine the preferred ledger based on its support + + @param current The current ledger the node follows + @param dist Ledger IDs and corresponding counts of support + @return The ID of the ledger with most support, preferring to stick with + current ledger in the case of equal support +*/ +template +inline LedgerID +getPreferredLedger( + LedgerID const& current, + hash_map const& dist) +{ + LedgerID netLgr = current; + int netLgrCount = 0; + for (auto const& it : dist) + { + // Switch to ledger supported by more peers + // On a tie, prefer the current ledger, or the one with higher ID + if ((it.second > netLgrCount) || + ((it.second == netLgrCount) && + ((it.first == current) || + (it.first > netLgr && netLgr != current)))) + { + netLgr = it.first; + netLgrCount = it.second; + } + } + return netLgr; +} + /** Maintains current and recent ledger validations. Manages storage and queries related to validations received on the network. @@ -192,6 +223,7 @@ class Validations decay_result_t; using NodeKey = decay_result_t; using NodeID = decay_result_t; + using SeqType = decay_result_t; using ScopedLock = std::lock_guard; @@ -397,11 +429,12 @@ class Validations Validation& oldVal = ins.first->second.val; LedgerID const previousLedgerID = ins.first->second.prevLedgerID; - std::uint32_t const oldSeq{oldVal.seq()}; - std::uint32_t const newSeq{val.seq()}; + SeqType const oldSeq{oldVal.seq()}; + SeqType const newSeq{val.seq()}; // Sequence of 0 indicates a missing sequence number - if (oldSeq && newSeq && oldSeq == newSeq) + if ((oldSeq != SeqType{0}) && (newSeq != SeqType{0}) && + oldSeq == newSeq) { result = AddOutcome::sameSeq; @@ -491,7 +524,9 @@ class Validations ledgers one away from the current ledger to count as the current. @param currentLedger The identifier of the ledger we believe is current + (0 if unknown) @param priorLedger The identifier of our previous current ledger + (0 if unknown) @param cutoffBefore Ignore ledgers with sequence number before this @return Map representing the distribution of ledgerID by count @@ -500,10 +535,10 @@ class Validations currentTrustedDistribution( LedgerID const& currentLedger, LedgerID const& priorLedger, - std::uint32_t cutoffBefore) + SeqType cutoffBefore) { - bool const valCurrentLedger = currentLedger != beast::zero; - bool const valPriorLedger = priorLedger != beast::zero; + bool const valCurrentLedger = currentLedger != LedgerID{0}; + bool const valPriorLedger = priorLedger != LedgerID{0}; hash_map ret; @@ -524,8 +559,8 @@ class Validations if (!v.trusted()) return; - std::uint32_t const seq = v.seq(); - if ((seq == 0) || (seq >= cutoffBefore)) + SeqType const seq = v.seq(); + if ((seq == SeqType{0}) || (seq >= cutoffBefore)) { // contains a live record bool countPreferred = diff --git a/src/test/consensus/ByzantineFailureSim_test.cpp b/src/test/consensus/ByzantineFailureSim_test.cpp new file mode 100644 index 00000000000..81e883e31ab --- /dev/null +++ b/src/test/consensus/ByzantineFailureSim_test.cpp @@ -0,0 +1,104 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include +#include +#include +#include +#include + +namespace ripple { +namespace 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(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 network = a + b + c + d + e + f + g; + + StreamCollector sc{std::cout}; + + sim.collectors.add(sc); + + for (TrustGraph::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 transaciton + 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, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/consensus/Consensus_test.cpp b/src/test/consensus/Consensus_test.cpp index 669bbf9fc20..0459fe60f07 100644 --- a/src/test/consensus/Consensus_test.cpp +++ b/src/test/consensus/Consensus_test.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012-2016 Ripple Labs Inc-> + Copyright (c) 2012-2016 Ripple Labs Inc. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -37,7 +36,7 @@ class Consensus_test : public beast::unit_test::suite using namespace std::chrono_literals; // Use default parameters - ConsensusParms p; + ConsensusParms const p; beast::Journal j; // Bizarre times forcibly close @@ -76,10 +75,9 @@ class Consensus_test : public beast::unit_test::suite using namespace std::chrono_literals; // Use default parameterss - ConsensusParms p; + ConsensusParms const p; beast::Journal j; - // Not enough time has elapsed BEAST_EXPECT( ConsensusState::No == @@ -119,26 +117,22 @@ class Consensus_test : public beast::unit_test::suite using namespace std::chrono_literals; using namespace csf; - ConsensusParms parms; - auto tg = TrustGraph::makeComplete(1); - Sim s(parms, tg, topology(tg, fixed{parms.ledgerGRANULARITY})); - - auto& p = s.peers[0]; - - p.targetLedgers = 1; - p.start(); - p.submit(Tx{1}); + Sim s; + PeerGroup peers = s.createGroup(1); + Peer * peer = peers[0]; + peer->targetLedgers = 1; + peer->start(); + peer->submit(Tx{1}); - s.net.step(); + s.scheduler.step(); // Inspect that the proper ledger was created - BEAST_EXPECT(p.prevLedgerID().seq == 1); - BEAST_EXPECT(p.prevLedgerID() == p.lastClosedLedger.id()); - BEAST_EXPECT(p.lastClosedLedger.id().txs.size() == 1); - BEAST_EXPECT( - p.lastClosedLedger.id().txs.find(Tx{1}) != - p.lastClosedLedger.id().txs.end()); - BEAST_EXPECT(p.prevProposers() == 0); + auto const& lcl = peer->lastClosedLedger; + BEAST_EXPECT(peer->prevLedgerID() == lcl.id()); + BEAST_EXPECT(lcl.seq() == Ledger::Seq{1}); + BEAST_EXPECT(lcl.txs().size() == 1); + BEAST_EXPECT(lcl.txs().find(Tx{1}) != lcl.txs().end()); + BEAST_EXPECT(peer->prevProposers == 0); } void @@ -147,30 +141,34 @@ class Consensus_test : public beast::unit_test::suite using namespace csf; using namespace std::chrono; - ConsensusParms parms; - auto tg = TrustGraph::makeComplete(5); - Sim sim( - parms, - tg, - topology( - tg, - fixed{round(0.2 * parms.ledgerGRANULARITY)})); + ConsensusParms const parms; + Sim sim; + PeerGroup peers = sim.createGroup(5); - // everyone submits their own ID as a TX and relay it to peers - for (auto& p : sim.peers) - p.submit(Tx(p.id)); + // Connected trust and network graphs with single fixed delay + peers.trustAndConnect( + peers, round(0.2 * parms.ledgerGRANULARITY)); + + // everyone submits their own ID as a TX + for (Peer * p : peers) + p->submit(Tx(static_cast(p->id))); - // Verify all peers have the same LCL and it has all the Txs sim.run(1); - for (auto& p : sim.peers) + + // All peers are in sync + if (BEAST_EXPECT(sim.synchronized())) { - auto const& lgrID = p.prevLedgerID(); - BEAST_EXPECT(lgrID.seq == 1); - BEAST_EXPECT(p.prevProposers() == sim.peers.size() - 1); - for (std::uint32_t i = 0; i < sim.peers.size(); ++i) - BEAST_EXPECT(lgrID.txs.find(Tx{i}) != lgrID.txs.end()); - // Matches peer 0 ledger - BEAST_EXPECT(lgrID.txs == sim.peers[0].prevLedgerID().txs); + for (Peer const* peer : peers) + { + auto const& lcl = peer->lastClosedLedger; + BEAST_EXPECT(lcl.id() == peer->prevLedgerID()); + BEAST_EXPECT(lcl.seq() == Ledger::Seq{1}); + // All peers proposed + BEAST_EXPECT(peer->prevProposers == peers.size() - 1); + // All transactions were accepted + for (std::uint32_t i = 0; i < peers.size(); ++i) + BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end()); + } } } @@ -186,47 +184,54 @@ class Consensus_test : public beast::unit_test::suite // Test when a slow peer doesn't delay a consensus quorum (4/5 agree) { - ConsensusParms parms; - auto tg = TrustGraph::makeComplete(5); - - // Peers 0 is slow, 1-4 are fast - // This choice is based on parms.minCONSENSUS_PCT of 80 - Sim sim(parms, tg, topology(tg, [&](PeerID i, PeerID j) { - auto delayFactor = (i == 0 || j == 0) ? 1.1 : 0.2; - return round( - delayFactor * parms.ledgerGRANULARITY); - })); + ConsensusParms const parms; + Sim sim; + PeerGroup slow = sim.createGroup(1); + PeerGroup fast = sim.createGroup(4); + PeerGroup network = fast + slow; - // All peers submit their own ID as a transaction and relay it - // to peers - for (auto& p : sim.peers) - { - p.submit(Tx{p.id}); - } + // Fully connected trust graph + network.trust(network); + + // Fast and slow network connections + fast.connect( + fast, round(0.2 * parms.ledgerGRANULARITY)); + + slow.connect( + network, round(1.1 * parms.ledgerGRANULARITY)); + + // All peers submit their own ID as a transaction + for (Peer* peer : network) + peer->submit(Tx{static_cast(peer->id)}); sim.run(1); // Verify all peers have same LCL but are missing transaction 0 - // which was not received by all peers before the ledger closed - for (auto& p : sim.peers) + // All peers are in sync even with a slower peer 0 + if (BEAST_EXPECT(sim.synchronized())) { - auto const& lgrID = p.prevLedgerID(); - BEAST_EXPECT(lgrID.seq == 1); + for (Peer* peer : network) + { + auto const& lcl = peer->lastClosedLedger; + BEAST_EXPECT(lcl.id() == peer->prevLedgerID()); + BEAST_EXPECT(lcl.seq() == Ledger::Seq{1}); + + BEAST_EXPECT(peer->prevProposers == network.size() - 1); + BEAST_EXPECT( + peer->prevRoundTime == network[0]->prevRoundTime); - BEAST_EXPECT(p.prevProposers() == sim.peers.size() - 1); - BEAST_EXPECT(p.prevRoundTime() == sim.peers[0].prevRoundTime()); + BEAST_EXPECT(lcl.txs().find(Tx{0}) == lcl.txs().end()); + for (std::uint32_t i = 2; i < network.size(); ++i) + BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end()); - BEAST_EXPECT(lgrID.txs.find(Tx{0}) == lgrID.txs.end()); - for (std::uint32_t i = 2; i < sim.peers.size(); ++i) - BEAST_EXPECT(lgrID.txs.find(Tx{i}) != lgrID.txs.end()); - // Matches peer 0 ledger - BEAST_EXPECT(lgrID.txs == sim.peers[0].prevLedgerID().txs); + // Tx 0 didn't make it + BEAST_EXPECT( + peer->openTxs.find(Tx{0}) != peer->openTxs.end()); + } } - BEAST_EXPECT( - sim.peers[0].openTxs.find(Tx{0}) != sim.peers[0].openTxs.end()); } - // Test when the slow peers delay a consensus quorum (4/6 agree) + // Test when the slow peers delay a consensus quorum (4/6 agree) { // Run two tests // 1. The slow peers are participating in consensus @@ -234,88 +239,100 @@ class Consensus_test : public beast::unit_test::suite for (auto isParticipant : {true, false}) { - ConsensusParms parms; - auto tg = TrustGraph::makeComplete(6); - - // Peers 0,1 are slow, 2-5 are fast - // This choice is based on parms.minCONSENSUS_PCT of 80 - Sim sim( - parms, tg, topology(tg, [&](PeerID i, PeerID j) { - auto delayFactor = (i <= 1 || j <= 1) ? 1.1 : 0.2; - return round( - delayFactor * parms.ledgerGRANULARITY); - })); + ConsensusParms const parms; + + Sim sim; + PeerGroup slow = sim.createGroup(2); + PeerGroup fast = sim.createGroup(4); + PeerGroup network = fast + slow; + + // Connected trust graph + network.trust(network); + + // Fast and slow network connections + fast.connect( + fast, round(0.2 * parms.ledgerGRANULARITY)); - sim.peers[0].proposing_ = sim.peers[0].validating_ = - isParticipant; - sim.peers[1].proposing_ = sim.peers[1].validating_ = - isParticipant; + slow.connect( + network, round(1.1 * parms.ledgerGRANULARITY)); + + for (Peer* peer : slow) + peer->runAsValidator = isParticipant; // All peers submit their own ID as a transaction and relay it // to peers - for (auto& p : sim.peers) - { - p.submit(Tx{p.id}); - } + for (Peer* peer : network) + peer->submit(Tx{static_cast(peer->id)}); sim.run(1); - // Verify all peers have same LCL but are missing transaction 0 - // which was not received by all peers before the ledger closed - for (auto& p : sim.peers) + if (BEAST_EXPECT(sim.synchronized())) { - auto const& lgrID = p.prevLedgerID(); - BEAST_EXPECT(lgrID.seq == 1); + // Verify all peers have same LCL but are missing + // transaction 0,1 which was not received by all peers before + // the ledger closed + for (Peer* peer : network) + { + // Closed ledger has all but transaction 0,1 + auto const& lcl = peer->lastClosedLedger; + BEAST_EXPECT(lcl.seq() == Ledger::Seq{1}); + BEAST_EXPECT(lcl.txs().find(Tx{0}) == lcl.txs().end()); + BEAST_EXPECT(lcl.txs().find(Tx{1}) == lcl.txs().end()); + for (std::uint32_t i = slow.size(); i < network.size(); + ++i) + BEAST_EXPECT( + lcl.txs().find(Tx{i}) != lcl.txs().end()); + + // Tx 0-1 didn't make it + BEAST_EXPECT( + peer->openTxs.find(Tx{0}) != peer->openTxs.end()); + BEAST_EXPECT( + peer->openTxs.find(Tx{1}) != peer->openTxs.end()); + } - // If peer 0,1 are participating + Peer const* slowPeer = slow[0]; if (isParticipant) + BEAST_EXPECT( + slowPeer->prevProposers == network.size() - 1); + else + BEAST_EXPECT(slowPeer->prevProposers == fast.size()); + + for (Peer* peer : fast) { - BEAST_EXPECT(p.prevProposers() == sim.peers.size() - 1); // Due to the network link delay settings // Peer 0 initially proposes {0} // Peer 1 initially proposes {1} // Peers 2-5 initially propose {2,3,4,5} - // Since peers 2-5 agree, 4/6 > the initial 50% - // threshold on disputed transactions, so Peer 0 and 1 - // change their position to match peers 2-5 and declare - // consensus now that 5/6 proposed positions match - // (themselves and peers 2-5). + // Since peers 2-5 agree, 4/6 > the initial 50% needed + // to include a disputed transaction, so Peer 0/1 switch + // to agree with those peers. Peer 0/1 then closes with + // an 80% quorum of agreeing positions (5/6) match. // // Peers 2-5 do not change position, since tx 0 or tx 1 - // have less than the 50% initial threshold. They also - // cannot declare consensus, since 4/6 < 80% threshold - // agreement on current positions. Instead, they have - // to wait an additional timerEntry call for the updated - // peer 0 and peer 1 positions to arrive. Once they do, - // now peers 2-5 see complete agreement and declare - // consensus - if (p.id > 1) + // have less than the 50% initial threshold. They also + // cannot declare consensus, since 4/6 agreeing + // positions are < 80% threshold. They therefore need an + // additional timerEntry call to see the updated + // positions from Peer 0 & 1. + + if (isParticipant) + { BEAST_EXPECT( - p.prevRoundTime() > - sim.peers[0].prevRoundTime()); - } - else // peer 0,1 are not participating - { - auto const proposers = p.prevProposers(); - if (p.id <= 1) - BEAST_EXPECT(proposers == sim.peers.size() - 2); + peer->prevProposers == network.size() - 1); + BEAST_EXPECT( + peer->prevRoundTime > slowPeer->prevRoundTime); + } else - BEAST_EXPECT(proposers == sim.peers.size() - 3); - - // so all peers should have closed together - BEAST_EXPECT( - p.prevRoundTime() == sim.peers[0].prevRoundTime()); + { + BEAST_EXPECT( + peer->prevProposers == fast.size() - 1); + // so all peers should have closed together + BEAST_EXPECT( + peer->prevRoundTime == slowPeer->prevRoundTime); + } } - - BEAST_EXPECT(lgrID.txs.find(Tx{0}) == lgrID.txs.end()); - for (std::uint32_t i = 2; i < sim.peers.size(); ++i) - BEAST_EXPECT(lgrID.txs.find(Tx{i}) != lgrID.txs.end()); - // Matches peer 0 ledger - BEAST_EXPECT(lgrID.txs == sim.peers[0].prevLedgerID().txs); } - BEAST_EXPECT( - sim.peers[0].openTxs.find(Tx{0}) != - sim.peers[0].openTxs.end()); + } } } @@ -327,19 +344,20 @@ class Consensus_test : public beast::unit_test::suite using namespace std::chrono; // This is a very specialized test to get ledgers to disagree on - // the close time. It unfortunately assumes knowledge about current - // timing constants. This is a necessary evil to get coverage up + // the close time. It unfortunately assumes knowledge about current + // timing constants. This is a necessary evil to get coverage up // pending more extensive refactorings of timing constants. // In order to agree-to-disagree on the close time, there must be no - // clear majority of nodes agreeing on a close time. This test + // clear majority of nodes agreeing on a close time. This test // sets a relative offset to the peers internal clocks so that they // send proposals with differing times. - // However, they have to agree on the effective close time, not the - // exact close time. The minimum closeTimeResolution is given by + // However, agreement is on the effective close time, not the + // exact close time. The minimum closeTimeResolution is given by // ledgerPossibleTimeResolutions[0], which is currently 10s. This means - // the skews need to be at least 10 seconds. + // the skews need to be at least 10 seconds to have different effective + // close times. // Complicating this matter is that nodes will ignore proposals // with times more than proposeFRESHNESS =20s in the past. So at @@ -347,35 +365,41 @@ class Consensus_test : public beast::unit_test::suite // (0s,10s,20s). // This test therefore has 6 nodes, with 2 nodes having each type of - // skew. Then no majority (1/3 < 1/2) of nodes will agree on an + // skew. Then no majority (1/3 < 1/2) of nodes will agree on an // actual close time. - ConsensusParms parms; - auto tg = TrustGraph::makeComplete(6); - Sim sim( - parms, - tg, - topology( - tg, - fixed{round(0.2 * parms.ledgerGRANULARITY)})); + ConsensusParms const parms; + Sim sim; + + PeerGroup groupA = sim.createGroup(2); + PeerGroup groupB = sim.createGroup(2); + PeerGroup groupC = sim.createGroup(2); + PeerGroup network = groupA + groupB + groupC; + + network.trust(network); + network.connect( + network, round(0.2 * parms.ledgerGRANULARITY)); // Run consensus without skew until we have a short close time // resolution - while (sim.peers.front().lastClosedLedger.closeTimeResolution() >= + Peer* firstPeer = *groupA.begin(); + while (firstPeer->lastClosedLedger.closeTimeResolution() >= parms.proposeFRESHNESS) sim.run(1); - // Introduce a shift on the time of half the peers - sim.peers[0].clockSkew = parms.proposeFRESHNESS / 2; - sim.peers[1].clockSkew = parms.proposeFRESHNESS / 2; - sim.peers[2].clockSkew = parms.proposeFRESHNESS; - sim.peers[3].clockSkew = parms.proposeFRESHNESS; + // Introduce a shift on the time of 2/3 of peers + for (Peer* peer : groupA) + peer->clockSkew = parms.proposeFRESHNESS / 2; + for (Peer* peer : groupB) + peer->clockSkew = parms.proposeFRESHNESS; - // Verify all peers have the same LCL and it has all the Txs sim.run(1); - for (auto& p : sim.peers) + + // All nodes agreed to disagree on the close time + if (BEAST_EXPECT(sim.synchronized())) { - BEAST_EXPECT(!p.lastClosedLedger.closeAgree()); + for (Peer* peer : network) + BEAST_EXPECT(!peer->lastClosedLedger.closeAgree()); } } @@ -387,21 +411,21 @@ class Consensus_test : public beast::unit_test::suite // Specialized test to exercise a temporary fork in which some peers // are working on an incorrect prior ledger. - ConsensusParms parms; - + ConsensusParms const parms; // Vary the time it takes to process validations to exercise detecting // the wrong LCL at different phases of consensus for (auto validationDelay : {0ms, parms.ledgerMIN_CLOSE}) { // Consider 10 peers: - // 0 1 2 3 4 5 6 7 8 9 + // 0 1 2 3 4 5 6 7 8 9 + // minority majorityA majorityB // // Nodes 0-1 trust nodes 0-4 // Nodes 2-9 trust nodes 2-9 // // By submitting tx 0 to nodes 0-4 and tx 1 to nodes 5-9, - // nodes 0-1 will generate the wrong LCL (with tx 0). The remaining + // nodes 0-1 will generate the wrong LCL (with tx 0). The remaining // nodes will instead accept the ledger with tx 1. // Nodes 0-1 will detect this mismatch during a subsequent round @@ -409,38 +433,41 @@ class Consensus_test : public beast::unit_test::suite // Nodes 0-1 will acquire the proper ledger from the network and // resume consensus and eventually generate the dominant network - // ledger + // ledger. + + // This topology can potentially fork with the above trust relations + // but that is intended for this test. + + Sim sim; + + PeerGroup minority = sim.createGroup(2); + PeerGroup majorityA = sim.createGroup(3); + PeerGroup majorityB = sim.createGroup(5); - std::vector unls; - unls.push_back({2, 3, 4, 5, 6, 7, 8, 9}); - unls.push_back({0, 1, 2, 3, 4}); - std::vector membership(10, 0); - membership[0] = 1; - membership[1] = 1; + PeerGroup majority = majorityA + majorityB; + PeerGroup network = minority + majority; - TrustGraph tg{unls, membership}; + SimDuration delay = + round(0.2 * parms.ledgerGRANULARITY); + minority.trustAndConnect(minority + majorityA, delay); + majority.trustAndConnect(majority, delay); - // This topology can fork, which is why we are using it for this - // test. - BEAST_EXPECT(tg.canFork(parms.minCONSENSUS_PCT / 100.)); + CollectByNode jumps; + sim.collectors.add(jumps); - auto netDelay = round(0.2 * parms.ledgerGRANULARITY); - Sim sim(parms, tg, topology(tg, fixed{netDelay})); + BEAST_EXPECT(sim.trustGraph.canFork(parms.minCONSENSUS_PCT / 100.)); // initial round to set prior state sim.run(1); // Nodes in smaller UNL have seen tx 0, nodes in other unl have seen // tx 1 - for (auto& p : sim.peers) - { - p.validationDelay = validationDelay; - p.missingLedgerDelay = netDelay; - if (unls[1].find(p.id) != unls[1].end()) - p.openTxs.insert(Tx{0}); - else - p.openTxs.insert(Tx{1}); - } + for (Peer* peer : network) + peer->delays.recvValidation = validationDelay; + for (Peer* peer : (minority + majorityA)) + peer->openTxs.insert(Tx{0}); + for (Peer* peer : majorityB) + peer->openTxs.insert(Tx{1}); // Run for additional rounds // With no validation delay, only 2 more rounds are needed. @@ -454,70 +481,99 @@ class Consensus_test : public beast::unit_test::suite // 1. Round to generate different ledgers // 2. Round to detect different prior ledgers (but still generate // wrong ones) but end up declaring consensus on wrong LCL (but - // with the right transaction set!). This is because we detect + // with the right transaction set!). This is because we detect // the wrong LCL after we have closed the ledger, so we declare // consensus based solely on our peer proposals. But we haven't - // had time to acquire the right LCL + // had time to acquire the right ledger. // 3. Round to correct sim.run(3); - bc::flat_map> ledgers; - for (auto& p : sim.peers) + // The network never actually forks, since node 0-1 never see a + // quorum of validations to fully validate the incorrect chain. + + // However, for a non zero-validation delay, the network is not + // synchronized because nodes 0 and 1 are running one ledger behind + if (BEAST_EXPECT(sim.branches() == 1)) { - for (auto const& l : p.ledgers) + for(Peer const* peer : majority) { - ledgers[l.first.seq].insert(l.first); + // No jumps for majority nodes + BEAST_EXPECT(jumps[peer->id].closeJumps.empty()); + BEAST_EXPECT(jumps[peer->id].fullyValidatedJumps.empty()); + } + for(Peer const* peer : minority) + { + auto & peerJumps = jumps[peer->id]; + // last closed ledger jump between chains + { + if (BEAST_EXPECT(peerJumps.closeJumps.size() == 1)) + { + JumpCollector::Jump const& jump = + peerJumps.closeJumps.front(); + // Jump is to a different chain + BEAST_EXPECT(jump.from.seq() <= jump.to.seq()); + BEAST_EXPECT( + !sim.oracle.isAncestor(jump.from, jump.to)); + } + } + // fully validated jump forward in same chain + { + if (BEAST_EXPECT( + peerJumps.fullyValidatedJumps.size() == 1)) + { + JumpCollector::Jump const& jump = + peerJumps.fullyValidatedJumps.front(); + // Jump is to a different chain with same seq + BEAST_EXPECT(jump.from.seq() < jump.to.seq()); + BEAST_EXPECT( + sim.oracle.isAncestor(jump.from, jump.to)); + } + } } - } - - BEAST_EXPECT(ledgers[0].size() == 1); - BEAST_EXPECT(ledgers[1].size() == 1); - if (validationDelay == 0s) - { - BEAST_EXPECT(ledgers[2].size() == 2); - BEAST_EXPECT(ledgers[3].size() == 1); - BEAST_EXPECT(ledgers[4].size() == 1); - } - else - { - BEAST_EXPECT(ledgers[2].size() == 2); - BEAST_EXPECT(ledgers[3].size() == 2); - BEAST_EXPECT(ledgers[4].size() == 1); } } - // Additional test engineered to switch LCL during the establish phase. - // This was added to trigger a scenario that previously crashed, in which - // switchLCL switched from establish to open phase, but still processed - // the establish phase logic. + { - // A mostly disjoint topology - std::vector unls; - unls.push_back({0, 1}); - unls.push_back({2}); - unls.push_back({3}); - unls.push_back({0, 1, 2, 3, 4}); - std::vector membership = {0, 0, 1, 2, 3}; - - TrustGraph tg{unls, membership}; - - Sim sim(parms, tg, topology(tg, fixed{round( - 0.2 * parms.ledgerGRANULARITY)})); - - // initial ground to set prior state - sim.run(1); - for (auto &p : sim.peers) { - // A long delay to acquire a missing ledger from the network - p.missingLedgerDelay = 2 * parms.ledgerMIN_CLOSE; - - // Everyone sees only their own LCL - p.openTxs.insert(Tx(p.id)); - } - // additional rounds to generate wrongLCL and recover - sim.run(2); - - // Check all peers recovered - for (auto &p : sim.peers) - BEAST_EXPECT(p.prevLedgerID() == sim.peers[0].prevLedgerID()); + // Additional test engineered to switch LCL during the establish + // phase. This was added to trigger a scenario that previously + // crashed, in which switchLCL switched from establish to open + // phase, but still processed the establish phase logic. + + // Loner node will accept an initial ledger A, but all other nodes + // accept ledger B a bit later. By delaying the time it takes + // to process a validation, loner node will detect the wrongLCL + // after it is already in the establish phase of the next round. + + Sim sim; + PeerGroup loner = sim.createGroup(1); + PeerGroup friends = sim.createGroup(3); + loner.trust(loner + friends); + + PeerGroup others = sim.createGroup(6); + PeerGroup clique = friends + others; + clique.trust(clique); + + PeerGroup network = loner + clique; + network.connect( + network, round(0.2 * parms.ledgerGRANULARITY)); + + // initial round to set prior state + sim.run(1); + for (Peer* peer : (loner + friends)) + peer->openTxs.insert(Tx(0)); + for (Peer* peer : others) + peer->openTxs.insert(Tx(1)); + + // Delay validation processing + for (Peer* peer : network) + peer->delays.recvValidation = parms.ledgerGRANULARITY; + + // additional rounds to generate wrongLCL and recover + sim.run(2); + + // Check all peers recovered + for (Peer * p: network) + BEAST_EXPECT(p->prevLedgerID() == network[0]->prevLedgerID()); } } @@ -535,25 +591,27 @@ class Consensus_test : public beast::unit_test::suite { ConsensusParms parms; parms.useRoundedCloseTime = useRoundedCloseTime; - std::vector unls; - unls.push_back({0,1,2,3,4,5}); - - std::vector membership(unls[0].size(), 0); - TrustGraph tg{unls, membership}; + Sim sim; // This requires a group of 4 fast and 2 slow peers to create a // situation in which a subset of peers requires seeing additional // proposals to declare consensus. + PeerGroup slow = sim.createGroup(2); + PeerGroup fast = sim.createGroup(4); + PeerGroup network = fast + slow; - Sim sim( - parms, - tg, - topology(tg, [&](PeerID i, PeerID j) { - auto delayFactor = (i <= 1 || j <= 1) ? 1.1 : 0.2; - return round( - delayFactor * parms.ledgerGRANULARITY); - })); + for (Peer* peer : network) + peer->consensusParms = parms; + + // Connected trust graph + network.trust(network); + + // Fast and slow network connections + fast.connect( + fast, round(0.2 * parms.ledgerGRANULARITY)); + slow.connect( + network, round(1.1 * parms.ledgerGRANULARITY)); // Run to the ledger *prior* to decreasing the resolution sim.run(increaseLedgerTimeResolutionEvery - 2); @@ -572,11 +630,11 @@ class Consensus_test : public beast::unit_test::suite // network delay settings, the round of consensus will take 5s, so // the next ledger's close time will - NetClock::duration when = sim.peers[0].now().time_since_epoch(); + NetClock::duration when = network[0]->now().time_since_epoch(); // Check we are before the 30s to 20s transition NetClock::duration resolution = - sim.peers[0].lastClosedLedger.closeTimeResolution(); + network[0]->lastClosedLedger.closeTimeResolution(); BEAST_EXPECT(resolution == NetClock::duration{30s}); while ( @@ -585,28 +643,26 @@ class Consensus_test : public beast::unit_test::suite when += 1s; // Advance the clock without consensus running (IS THIS WHAT // PREVENTS IT IN PRACTICE?) - sim.net.step_for( - NetClock::time_point{when} - sim.peers[0].now()); + sim.scheduler.step_for( + NetClock::time_point{when} - network[0]->now()); // Run one more ledger with 30s resolution sim.run(1); - - // close time should be ahead of clock time since we engineered - // the close time to round up - for (Peer const & peer : sim.peers) + if (BEAST_EXPECT(sim.synchronized())) { - BEAST_EXPECT( - peer.lastClosedLedger.closeTime() > peer.now()); - BEAST_EXPECT(peer.lastClosedLedger.closeAgree()); - BEAST_EXPECT( - peer.lastClosedLedger.id() == - sim.peers[0].lastClosedLedger.id()); + // close time should be ahead of clock time since we engineered + // the close time to round up + for (Peer* peer : network) + { + BEAST_EXPECT( + peer->lastClosedLedger.closeTime() > peer->now()); + BEAST_EXPECT(peer->lastClosedLedger.closeAgree()); + } } - // All peers submit their own ID as a transaction - for (Peer & peer : sim.peers) - peer.submit(Tx{static_cast(peer.id)}); + for (Peer* peer : network) + peer->submit(Tx{static_cast(peer->id)}); // Run 1 more round, this time it will have a decreased // resolution of 20 seconds. @@ -629,40 +685,51 @@ class Consensus_test : public beast::unit_test::suite sim.run(1); - for (Peer const& peer : sim.peers) + if (parms.useRoundedCloseTime) { - BEAST_EXPECT( - peer.lastClosedLedger.id() == - sim.peers[0].lastClosedLedger.id()); + BEAST_EXPECT(sim.synchronized()); } - - if (!parms.useRoundedCloseTime) + else { - auto const & slowLCL = sim.peers[0].lastClosedLedger; - auto const & fastLCL = sim.peers[2].lastClosedLedger; + // Not currently synchronized + BEAST_EXPECT(!sim.synchronized()); + + // All slow peers agreed on LCL + BEAST_EXPECT(std::all_of( + slow.begin(), slow.end(), [&slow](Peer const* p) { + return p->lastClosedLedger.id() == + slow[0]->lastClosedLedger.id(); + })); + + // All fast peers agreed on LCL + BEAST_EXPECT(std::all_of( + fast.begin(), fast.end(), [&fast](Peer const* p) { + return p->lastClosedLedger.id() == + fast[0]->lastClosedLedger.id(); + })); + Ledger const& slowLCL = slow[0]->lastClosedLedger; + Ledger const& fastLCL = fast[0]->lastClosedLedger; // Agree on parent close and close resolution BEAST_EXPECT( - slowLCL.parentCloseTime() == - fastLCL.parentCloseTime()); + slowLCL.parentCloseTime() == fastLCL.parentCloseTime()); BEAST_EXPECT( slowLCL.closeTimeResolution() == fastLCL.closeTimeResolution()); - auto parentClose = slowLCL.parentCloseTime(); - auto closeResolution = - slowLCL.closeTimeResolution(); - - auto slowClose = sim.peers[0].lastClosedLedger.closeTime(); - auto fastClose = sim.peers[2].lastClosedLedger.closeTime(); - // Close times disagree ... - BEAST_EXPECT(slowClose != fastClose); + BEAST_EXPECT(slowLCL.closeTime() != fastLCL.closeTime()); // Effective close times agree! The slow peer already rounded! BEAST_EXPECT( - effCloseTime(slowClose, closeResolution, parentClose) == - effCloseTime(fastClose, closeResolution, parentClose)); + effCloseTime( + slowLCL.closeTime(), + slowLCL.closeTimeResolution(), + slowLCL.parentCloseTime()) == + effCloseTime( + fastLCL.closeTime(), + fastLCL.closeTimeResolution(), + fastLCL.parentCloseTime())); } } } @@ -673,157 +740,89 @@ class Consensus_test : public beast::unit_test::suite using namespace csf; using namespace std::chrono; - int numPeers = 10; - for (int overlap = 0; overlap <= numPeers; ++overlap) + std::uint32_t numPeers = 10; + // Vary overlap between two UNLs + for (std::uint32_t overlap = 0; overlap <= numPeers; ++overlap) { - ConsensusParms parms; - auto tg = TrustGraph::makeClique(numPeers, overlap); - Sim sim( - parms, - tg, - topology( - tg, - fixed{ - round(0.2 * parms.ledgerGRANULARITY)})); + ConsensusParms const parms; + Sim sim; + + std::uint32_t numA = (numPeers - overlap) / 2; + std::uint32_t numB = numPeers - numA - overlap; + + PeerGroup aOnly = sim.createGroup(numA); + PeerGroup bOnly = sim.createGroup(numB); + PeerGroup commonOnly = sim.createGroup(overlap); + + PeerGroup a = aOnly + commonOnly; + PeerGroup b = bOnly + commonOnly; + + PeerGroup network = a + b; + + SimDuration delay = + round(0.2 * parms.ledgerGRANULARITY); + a.trustAndConnect(a, delay); + b.trustAndConnect(b, delay); // Initial round to set prior state sim.run(1); - for (auto& p : sim.peers) + for (Peer* peer : network) { // Nodes have only seen transactions from their neighbors - p.openTxs.insert(Tx{p.id}); - for (auto const link : sim.net.links(&p)) - p.openTxs.insert(Tx{link.to->id}); + peer->openTxs.insert(Tx{static_cast(peer->id)}); + for (Peer* to : sim.trustGraph.trustedPeers(peer)) + peer->openTxs.insert( + Tx{static_cast(to->id)}); } sim.run(1); - // See if the network forked - bc::flat_set ledgers; - for (auto& p : sim.peers) - { - ledgers.insert(p.prevLedgerID()); - } - // Fork should not happen for 40% or greater overlap // Since the overlapped nodes have a UNL that is the union of the // two cliques, the maximum sized UNL list is the number of peers if (overlap > 0.4 * numPeers) - BEAST_EXPECT(ledgers.size() == 1); - else // Even if we do fork, there shouldn't be more than 3 ledgers - // One for cliqueA, one for cliqueB and one for nodes in both - BEAST_EXPECT(ledgers.size() <= 3); - } - } - - void - simClockSkew() - { - using namespace csf; - using namespace std::chrono_literals; - // Attempting to test what happens if peers enter consensus well - // separated in time. Initial round (in which peers are not staggered) - // is used to get the network going, then transactions are submitted - // together and consensus continues. - - // For all the times below, the same ledger is built but the close times - // disgree. BUT THE LEDGER DOES NOT SHOW disagreeing close times. - // It is probably because peer proposals are stale, so they get ignored - // but with no peer proposals, we always assume close time consensus is - // true. - - // Disabled while continuing to understand testt. - - for (auto stagger : {800ms, 1600ms, 3200ms, 30000ms, 45000ms, 300000ms}) - { - ConsensusParms parms; - auto tg = TrustGraph::makeComplete(5); - Sim sim(parms, tg, topology(tg, [](PeerID i, PeerID) { - return 200ms * (i + 1); - })); - - // all transactions submitted before starting - // Initial round to set prior state - sim.run(1); - - for (auto& p : sim.peers) - { - p.openTxs.insert(Tx{0}); - p.targetLedgers = p.completedLedgers + 1; - } - - // stagger start of consensus - for (auto& p : sim.peers) + BEAST_EXPECT(sim.synchronized()); + else { - p.start(); - sim.net.step_for(stagger); + // Even if we do fork, there shouldn't be more than 3 ledgers + // One for cliqueA, one for cliqueB and one for nodes in both + BEAST_EXPECT(sim.branches() <= 3); } - - // run until all peers have accepted all transactions - sim.net.step_while([&]() { - for (auto& p : sim.peers) - { - if (p.prevLedgerID().txs.size() != 1) - { - return true; - } - } - return false; - }); } } void - simScaleFree() + testHubNetwork() { - using namespace std::chrono; using namespace csf; - // Generate a quasi-random scale free network and simulate consensus - // for a single transaction - - int N = 100; // Peers - - int numUNLs = 15; // UNL lists - int minUNLSize = N / 4, maxUNLSize = N / 2; + using namespace std::chrono; - double transProb = 0.5; + // Simulate a set of 5 validators that aren't directly connected but + // rely on a single hub node for communication - std::mt19937_64 rng; - ConsensusParms parms; + ConsensusParms const parms; + Sim sim; + PeerGroup validators = sim.createGroup(5); + PeerGroup center = sim.createGroup(1); + validators.trust(validators); + center.trust(validators); - auto tg = TrustGraph::makeRandomRanked( - N, - numUNLs, - PowerLawDistribution{1, 3}, - std::uniform_int_distribution<>{minUNLSize, maxUNLSize}, - rng); + SimDuration delay = + round(0.2 * parms.ledgerGRANULARITY); + validators.connect(center, delay); - Sim sim{ - parms, - tg, - topology( - tg, - fixed{round(0.2 * parms.ledgerGRANULARITY)})}; + center[0]->runAsValidator = false; - // Initial round to set prior state + // prep round to set initial state. sim.run(1); - std::uniform_real_distribution<> u{}; - for (auto& p : sim.peers) - { - // 50-50 chance to have seen a transaction - if (u(rng) >= transProb) - p.openTxs.insert(Tx{0}); - } - 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(p->id))); - // See if the network forked - bc::flat_set ledgers; - for (auto& p : sim.peers) - { - ledgers.insert(p.prevLedgerID()); - } + sim.run(1); - BEAST_EXPECT(ledgers.size() == 1); + // All peers are in sync + BEAST_EXPECT(sim.synchronized()); } void @@ -839,12 +838,10 @@ class Consensus_test : public beast::unit_test::suite testWrongLCL(); testConsensusCloseTimeRounding(); testFork(); - - simClockSkew(); - simScaleFree(); + testHubNetwork(); } }; BEAST_DEFINE_TESTSUITE(Consensus, consensus, ripple); -} // test -} // ripple +} // namespace test +} // namespace ripple diff --git a/src/test/consensus/DistributedValidatorsSim_test.cpp b/src/test/consensus/DistributedValidatorsSim_test.cpp new file mode 100644 index 00000000000..5bdefc0211b --- /dev/null +++ b/src/test/consensus/DistributedValidatorsSim_test.cpp @@ -0,0 +1,275 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2016 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace 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); + + ConsensusParms const parms; + 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 minues, submitting 100 tx/second + std::chrono::nanoseconds const simDuration = 10min; + std::chrono::nanoseconds const quiet = 10s; + Rate const rate{100, 1000ms}; + + // Initialize timers + HeartbeatTimer heart(sim.scheduler); + + // txs, start/stop/step, target + auto peerSelector = makeSelector(peers.begin(), + peers.end(), + std::vector(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(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); + + ConsensusParms const parms; + Sim sim; + PeerGroup peers = sim.createGroup(numPeers); + + // complete trust graph + peers.trust(peers); + + // scale-free connect graph with fixed delay + std::vector 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 minues, submitting 100 tx/second + std::chrono::nanoseconds simDuration = 10min; + std::chrono::nanoseconds quiet = 10s; + Rate rate{100, 1000ms}; + + // Initialize timers + HeartbeatTimer heart(sim.scheduler); + + // txs, start/stop/step, target + auto peerSelector = makeSelector(peers.begin(), + peers.end(), + std::vector(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(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(DistributedValidators, consensus, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/consensus/LedgerTiming_test.cpp b/src/test/consensus/LedgerTiming_test.cpp index b3ce73733df..8c036ab9d90 100644 --- a/src/test/consensus/LedgerTiming_test.cpp +++ b/src/test/consensus/LedgerTiming_test.cpp @@ -95,11 +95,32 @@ class LedgerTiming_test : public beast::unit_test::suite } + 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(); } }; diff --git a/src/test/consensus/ScaleFreeSim_test.cpp b/src/test/consensus/ScaleFreeSim_test.cpp new file mode 100644 index 00000000000..b2645e8b9c5 --- /dev/null +++ b/src/test/consensus/ScaleFreeSim_test.cpp @@ -0,0 +1,123 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2016 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace 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 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(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 minues, submitting 100 tx/second + std::chrono::nanoseconds const simDuration = 10min; + std::chrono::nanoseconds const quiet = 10s; + Rate const rate{100, 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(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(ScaleFreeSim, consensus, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/consensus/Validations_test.cpp b/src/test/consensus/Validations_test.cpp index 7c807ac457a..0f31210c681 100644 --- a/src/test/consensus/Validations_test.cpp +++ b/src/test/consensus/Validations_test.cpp @@ -17,9 +17,11 @@ */ //============================================================================== #include +#include #include #include #include +#include #include #include @@ -28,166 +30,10 @@ namespace ripple { namespace test { - +namespace csf { class Validations_test : public beast::unit_test::suite { using clock_type = beast::abstract_clock const; - //-------------------------------------------------------------------------- - // Basic type wrappers for validation types - - // Represents a ledger sequence number - struct Seq - { - explicit Seq(std::uint32_t sIn) : s{sIn} - { - } - - Seq() : s{0} - { - } - - operator std::uint32_t() const - { - return s; - } - - std::uint32_t s; - }; - - // Represents a unique ledger identifier - struct ID - { - explicit ID(std::uint32_t idIn) : id{idIn} - { - } - - ID() : id{0} - { - } - - int - signum() const - { - return id == 0 ? 0 : 1; - } - - operator std::size_t() const - { - return id; - } - - template - friend void - hash_append(Hasher& h, ID const& id) - { - using beast::hash_append; - hash_append(h, id.id); - } - - std::uint32_t id; - }; - - class Node; - - // Basic implementation of the requirements of Validation in the generic - // Validations class - class Validation - { - friend class Node; - - ID ledgerID_ = ID{0}; - Seq seq_ = Seq{0}; - NetClock::time_point signTime_; - NetClock::time_point seenTime_; - std::string key_; - std::size_t nodeID_ = 0; - bool trusted_ = true; - boost::optional loadFee_; - - public: - Validation() - { - } - - ID - ledgerID() const - { - return ledgerID_; - } - - Seq - seq() const - { - return seq_; - } - - NetClock::time_point - signTime() const - { - return signTime_; - } - - NetClock::time_point - seenTime() const - { - return seenTime_; - } - - std::string - key() const - { - return key_; - } - - std::uint32_t - nodeID() const - { - return nodeID_; - } - - bool - trusted() const - { - return trusted_; - } - - boost::optional - loadFee() const - { - return loadFee_; - } - - Validation const& - unwrap() const - { - return *this; - } - - auto - asTie() const - { - return std::tie( - ledgerID_, - seq_, - signTime_, - seenTime_, - key_, - nodeID_, - trusted_, - loadFee_); - } - bool - operator==(Validation const& o) const - { - return asTie() == o.asTie(); - } - - bool - operator<(Validation const& o) const - { - return asTie() < o.asTie(); - } - }; // Helper to convert steady_clock to a reasonable NetClock // This allows a single manual clock in the unit tests @@ -206,13 +52,13 @@ class Validations_test : public beast::unit_test::suite class Node { clock_type const& c_; - std::size_t nodeID_; + PeerID nodeID_; bool trusted_ = true; - std::size_t signIdx_ = 0; + std::size_t signIdx_ = 1; boost::optional loadFee_; public: - Node(std::uint32_t nodeID, clock_type const& c) : c_(c), nodeID_(nodeID) + Node(PeerID nodeID, clock_type const& c) : c_(c), nodeID_(nodeID) { } @@ -234,7 +80,7 @@ class Validations_test : public beast::unit_test::suite loadFee_ = fee; } - std::size_t + PeerID nodeID() const { return nodeID_; @@ -246,18 +92,17 @@ class Validations_test : public beast::unit_test::suite signIdx_++; } - std::string - masterKey() const + PeerKey + currKey() const { - return std::to_string(nodeID_); + return std::make_pair(nodeID_, signIdx_); } - std::string - currKey() const + PeerKey + masterKey() const { - return masterKey() + "_" + std::to_string(signIdx_); + return std::make_pair(nodeID_, 0); } - NetClock::time_point now() const { @@ -267,54 +112,30 @@ class Validations_test : public beast::unit_test::suite // Issue a new validation with given sequence number and id and // with signing and seen times offset from the common clock Validation - validation( - Seq seq, - ID i, + validation(Ledger::Seq seq, + Ledger::ID i, NetClock::duration signOffset, NetClock::duration seenOffset) const { - Validation v; - v.seq_ = seq; - v.ledgerID_ = i; - - v.signTime_ = now() + signOffset; - v.seenTime_ = now() + seenOffset; - - v.nodeID_ = nodeID_; - v.key_ = currKey(); - v.trusted_ = trusted_; - v.loadFee_ = loadFee_; - return v; + return Validation{i, seq, now() + signOffset, now() + seenOffset, + currKey(), nodeID_, trusted_, loadFee_}; } // Issue a new validation with the given sequence number and id Validation - validation(Seq seq, ID i) const + validation(Ledger::Seq seq, Ledger::ID i) const { return validation( seq, i, NetClock::duration{0}, NetClock::duration{0}); } }; - // Non-locking mutex to avoid the need for testing generic Validations - struct DummyMutex - { - void - lock() - { - } - - void - unlock() - { - } - }; // Saved StaleData for inspection in test struct StaleData { std::vector stale; - hash_map flushed; + hash_map flushed; }; // Generic Validations policy that saves stale/flushed data into @@ -325,8 +146,7 @@ class Validations_test : public beast::unit_test::suite clock_type& c_; public: - StalePolicy(StaleData& sd, clock_type& c) - : staleData_{sd}, c_{c} + StalePolicy(StaleData& sd, clock_type& c) : staleData_{sd}, c_{c} { } @@ -343,15 +163,28 @@ class Validations_test : public beast::unit_test::suite } void - flush(hash_map&& remaining) + flush(hash_map&& remaining) { staleData_.flushed = std::move(remaining); } }; + // Non-locking mutex to avoid locks in generic Validations + struct NotAMutex + { + void + lock() + { + } + + void + unlock() + { + } + }; + // Specialize generic Validations using the above types - using TestValidations = - Validations; + using TestValidations = Validations; // Hoist enum for writing simpler tests using AddOutcome = TestValidations::AddOutcome; @@ -365,7 +198,7 @@ class Validations_test : public beast::unit_test::suite beast::manual_clock clock_; beast::Journal j_; TestValidations tv_; - int nextNodeId_ = 0; + PeerID nextNodeId_{0}; public: TestHarness() : tv_(p_, clock_, j_, staleData_, clock_) @@ -417,7 +250,7 @@ class Validations_test : public beast::unit_test::suite return staleData_.stale; } - hash_map const& + hash_map const& flushed() const { return staleData_.flushed; @@ -434,7 +267,7 @@ class Validations_test : public beast::unit_test::suite Node a = harness.makeNode(); { { - auto const v = a.validation(Seq{1}, ID{1}); + auto const v = a.validation(Ledger::Seq{1}, Ledger::ID{1}); // Add a current validation BEAST_EXPECT(AddOutcome::current == harness.add(a, v)); @@ -448,42 +281,44 @@ class Validations_test : public beast::unit_test::suite // Replace with a new validation and ensure the old one is stale BEAST_EXPECT(harness.stale().empty()); - BEAST_EXPECT( - AddOutcome::current == harness.add(a, Seq{2}, ID{2})); + BEAST_EXPECT(AddOutcome::current == + harness.add(a, Ledger::Seq{2}, Ledger::ID{2})); BEAST_EXPECT(harness.stale().size() == 1); - BEAST_EXPECT(harness.stale()[0].ledgerID() == 1); + BEAST_EXPECT(harness.stale()[0].ledgerID() == Ledger::ID{1}); } { // Test the node changing signing key, then reissuing a ledger // Confirm old ledger on hand, but not new ledger - BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{2}) == 1); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{20}) == 0); + BEAST_EXPECT( + harness.vals().numTrustedForLedger(Ledger::ID{2}) == 1); + BEAST_EXPECT( + harness.vals().numTrustedForLedger(Ledger::ID{20}) == 0); // Issue a new signing key and re-issue the validation with a // new ID but the same sequence number a.advanceKey(); // No validations following ID{2} - BEAST_EXPECT(harness.vals().getNodesAfter(ID{2}) == 0); + BEAST_EXPECT(harness.vals().getNodesAfter(Ledger::ID{2}) == 0); BEAST_EXPECT( - AddOutcome::sameSeq == harness.add(a, Seq{2}, ID{20})); + AddOutcome::sameSeq == harness.add(a, Ledger::Seq{2}, Ledger::ID{20})); // Old ID should be gone ... - BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{2}) == 0); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{20}) == 1); + BEAST_EXPECT(harness.vals().numTrustedForLedger(Ledger::ID{2}) == 0); + BEAST_EXPECT(harness.vals().numTrustedForLedger(Ledger::ID{20}) == 1); { // Should be the only trusted for ID{20} auto trustedVals = - harness.vals().getTrustedForLedger(ID{20}); + harness.vals().getTrustedForLedger(Ledger::ID{20}); BEAST_EXPECT(trustedVals.size() == 1); BEAST_EXPECT(trustedVals[0].key() == a.currKey()); // ... and should be the only node after ID{2} - BEAST_EXPECT(harness.vals().getNodesAfter(ID{2}) == 1); + BEAST_EXPECT(harness.vals().getNodesAfter(Ledger::ID{2}) == 1); } @@ -492,35 +327,35 @@ class Validations_test : public beast::unit_test::suite a.advanceKey(); BEAST_EXPECT( - AddOutcome::sameSeq == harness.add(a, Seq{2}, ID{20})); + AddOutcome::sameSeq == harness.add(a, Ledger::Seq{2}, Ledger::ID{20})); { // Still the only trusted validation for ID{20} auto trustedVals = - harness.vals().getTrustedForLedger(ID{20}); + harness.vals().getTrustedForLedger(Ledger::ID{20}); BEAST_EXPECT(trustedVals.size() == 1); BEAST_EXPECT(trustedVals[0].key() == a.currKey()); // and still follows ID{2} since it was a re-issue - BEAST_EXPECT(harness.vals().getNodesAfter(ID{2}) == 1); + BEAST_EXPECT(harness.vals().getNodesAfter(Ledger::ID{2}) == 1); } } { // Processing validations out of order should ignore the older harness.clock().advance(2s); - auto const val3 = a.validation(Seq{3}, ID{3}); + auto const val3 = a.validation(Ledger::Seq{3}, Ledger::ID{3}); harness.clock().advance(4s); - auto const val4 = a.validation(Seq{4}, ID{4}); + auto const val4 = a.validation(Ledger::Seq{4}, Ledger::ID{4}); BEAST_EXPECT(AddOutcome::current == harness.add(a, val4)); BEAST_EXPECT(AddOutcome::stale == harness.add(a, val3)); // re-issued should not be added - auto const val4reissue = a.validation(Seq{4}, ID{44}); + auto const val4reissue = + a.validation(Ledger::Seq{4}, Ledger::ID{44}); BEAST_EXPECT(AddOutcome::stale == harness.add(a, val4reissue)); - } { // Process validations out of order with shifted times @@ -529,48 +364,32 @@ class Validations_test : public beast::unit_test::suite harness.clock().advance(1h); // Establish a new current validation - BEAST_EXPECT( - AddOutcome::current == harness.add(a, Seq{8}, ID{8})); + BEAST_EXPECT(AddOutcome::current == + harness.add(a, Ledger::Seq{8}, Ledger::ID{8})); // Process a validation that has "later" seq but early sign time - BEAST_EXPECT( - AddOutcome::stale == - harness.add(a, Seq{9}, ID{9}, -1s, -1s)); + BEAST_EXPECT(AddOutcome::stale == + harness.add(a, Ledger::Seq{9}, Ledger::ID{9}, -1s, -1s)); - // Process a validation that has an "earlier" seq but later sign time - BEAST_EXPECT( - AddOutcome::current == - harness.add(a, Seq{7}, ID{7}, 1s, 1s)); + // Process a validation that has an "earlier" seq but later sign + // time + BEAST_EXPECT(AddOutcome::current == + harness.add(a, Ledger::Seq{7}, Ledger::ID{7}, 1s, 1s)); } { // Test stale on arrival validations harness.clock().advance(1h); - BEAST_EXPECT( - AddOutcome::stale == - harness.add( - a, - Seq{15}, - ID{15}, - -harness.parms().validationCURRENT_EARLY, - 0s)); + BEAST_EXPECT(AddOutcome::stale == + harness.add(a, Ledger::Seq{15}, Ledger::ID{15}, + -harness.parms().validationCURRENT_EARLY, 0s)); - BEAST_EXPECT( - AddOutcome::stale == - harness.add( - a, - Seq{15}, - ID{15}, - harness.parms().validationCURRENT_WALL, - 0s)); + BEAST_EXPECT(AddOutcome::stale == + harness.add(a, Ledger::Seq{15}, Ledger::ID{15}, + harness.parms().validationCURRENT_WALL, 0s)); - BEAST_EXPECT( - AddOutcome::stale == - harness.add( - a, - Seq{15}, - ID{15}, - 0s, + BEAST_EXPECT(AddOutcome::stale == + harness.add(a, Ledger::Seq{15}, Ledger::ID{15}, 0s, harness.parms().validationCURRENT_LOCAL)); } } @@ -583,7 +402,8 @@ class Validations_test : public beast::unit_test::suite TestHarness harness; Node a = harness.makeNode(); - BEAST_EXPECT(AddOutcome::current == harness.add(a, Seq{1}, ID{1})); + BEAST_EXPECT(AddOutcome::current == + harness.add(a, Ledger::Seq{1}, Ledger::ID{1})); harness.vals().currentTrusted(); BEAST_EXPECT(harness.stale().empty()); harness.clock().advance(harness.parms().validationCURRENT_LOCAL); @@ -592,7 +412,7 @@ class Validations_test : public beast::unit_test::suite harness.vals().currentTrusted(); BEAST_EXPECT(harness.stale().size() == 1); - BEAST_EXPECT(harness.stale()[0].ledgerID() == 1); + BEAST_EXPECT(harness.stale()[0].ledgerID() == Ledger::ID{1}); } void @@ -610,24 +430,29 @@ class Validations_test : public beast::unit_test::suite // first round a,b,c agree, d has differing id for (auto const& node : {a, b, c}) - BEAST_EXPECT( - AddOutcome::current == harness.add(node, Seq{1}, ID{1})); - BEAST_EXPECT(AddOutcome::current == harness.add(d, Seq{1}, ID{10})); + BEAST_EXPECT(AddOutcome::current == + harness.add(node, Ledger::Seq{1}, Ledger::ID{1})); + BEAST_EXPECT(AddOutcome::current == + harness.add(d, Ledger::Seq{1}, Ledger::ID{10})); // Nothing past ledger 1 yet - BEAST_EXPECT(harness.vals().getNodesAfter(ID{1}) == 0); + BEAST_EXPECT(harness.vals().getNodesAfter(Ledger::ID{1}) == 0); harness.clock().advance(5s); // a and b have the same prior id, but b has a different current id // c is untrusted but on the same prior id // d has a different prior id - BEAST_EXPECT(AddOutcome::current == harness.add(a, Seq{2}, ID{2})); - BEAST_EXPECT(AddOutcome::current == harness.add(b, Seq{2}, ID{20})); - BEAST_EXPECT(AddOutcome::current == harness.add(c, Seq{2}, ID{2})); - BEAST_EXPECT(AddOutcome::current == harness.add(d, Seq{2}, ID{2})); - - BEAST_EXPECT(harness.vals().getNodesAfter(ID{1}) == 2); + BEAST_EXPECT(AddOutcome::current == + harness.add(a, Ledger::Seq{2}, Ledger::ID{2})); + BEAST_EXPECT(AddOutcome::current == + harness.add(b, Ledger::Seq{2}, Ledger::ID{20})); + BEAST_EXPECT(AddOutcome::current == + harness.add(c, Ledger::Seq{2}, Ledger::ID{2})); + BEAST_EXPECT(AddOutcome::current == + harness.add(d, Ledger::Seq{2}, Ledger::ID{2})); + + BEAST_EXPECT(harness.vals().getNodesAfter(Ledger::ID{1}) == 2); } void @@ -640,24 +465,30 @@ class Validations_test : public beast::unit_test::suite Node a = harness.makeNode(), b = harness.makeNode(); b.untrust(); - BEAST_EXPECT(AddOutcome::current == harness.add(a, Seq{1}, ID{1})); - BEAST_EXPECT(AddOutcome::current == harness.add(b, Seq{1}, ID{3})); + BEAST_EXPECT(AddOutcome::current == + harness.add(a, Ledger::Seq{1}, Ledger::ID{1})); + BEAST_EXPECT(AddOutcome::current == + harness.add(b, Ledger::Seq{1}, Ledger::ID{3})); // Only a is trusted BEAST_EXPECT(harness.vals().currentTrusted().size() == 1); - BEAST_EXPECT(harness.vals().currentTrusted()[0].ledgerID() == ID{1}); - BEAST_EXPECT(harness.vals().currentTrusted()[0].seq() == Seq{1}); + BEAST_EXPECT( + harness.vals().currentTrusted()[0].ledgerID() == Ledger::ID{1}); + BEAST_EXPECT( + harness.vals().currentTrusted()[0].seq() == Ledger::Seq{1}); harness.clock().advance(3s); for (auto const& node : {a, b}) - BEAST_EXPECT( - AddOutcome::current == harness.add(node, Seq{2}, ID{2})); + BEAST_EXPECT(AddOutcome::current == + harness.add(node, Ledger::Seq{2}, Ledger::ID{2})); // New validation for a BEAST_EXPECT(harness.vals().currentTrusted().size() == 1); - BEAST_EXPECT(harness.vals().currentTrusted()[0].ledgerID() == ID{2}); - BEAST_EXPECT(harness.vals().currentTrusted()[0].seq() == Seq{2}); + BEAST_EXPECT( + harness.vals().currentTrusted()[0].ledgerID() == Ledger::ID{2}); + BEAST_EXPECT( + harness.vals().currentTrusted()[0].seq() == Ledger::Seq{2}); // Pass enough time for it to go stale harness.clock().advance(harness.parms().validationCURRENT_LOCAL); @@ -675,14 +506,13 @@ class Validations_test : public beast::unit_test::suite b.untrust(); for (auto const& node : {a, b}) - BEAST_EXPECT( - AddOutcome::current == harness.add(node, Seq{1}, ID{1})); + BEAST_EXPECT(AddOutcome::current == + harness.add(node, Ledger::Seq{1}, Ledger::ID{1})); { - hash_set const expectedKeys = {a.masterKey(), - b.masterKey()}; - BEAST_EXPECT( - harness.vals().getCurrentPublicKeys() == expectedKeys); + hash_set const expectedKeys = { + a.masterKey(), b.masterKey()}; + BEAST_EXPECT(harness.vals().getCurrentPublicKeys() == expectedKeys); } harness.clock().advance(3s); @@ -692,14 +522,13 @@ class Validations_test : public beast::unit_test::suite b.advanceKey(); for (auto const& node : {a, b}) - BEAST_EXPECT( - AddOutcome::current == harness.add(node, Seq{2}, ID{2})); + BEAST_EXPECT(AddOutcome::current == + harness.add(node, Ledger::Seq{2}, Ledger::ID{2})); { - hash_set const expectedKeys = {a.masterKey(), - b.masterKey()}; - BEAST_EXPECT( - harness.vals().getCurrentPublicKeys() == expectedKeys); + hash_set const expectedKeys = { + a.masterKey(), b.masterKey()}; + BEAST_EXPECT(harness.vals().getCurrentPublicKeys() == expectedKeys); } // Pass enough time for them to go stale @@ -727,61 +556,70 @@ class Validations_test : public beast::unit_test::suite // goldilocks on seq 2, but is not trusted for (auto const& node : {baby, papa, mama, goldilocks}) - BEAST_EXPECT( - AddOutcome::current == harness.add(node, Seq{1}, ID{1})); + BEAST_EXPECT(AddOutcome::current == + harness.add(node, Ledger::Seq{1}, Ledger::ID{1})); harness.clock().advance(1s); for (auto const& node : {baby, mama, goldilocks}) - BEAST_EXPECT( - AddOutcome::current == harness.add(node, Seq{2}, ID{2})); + BEAST_EXPECT(AddOutcome::current == + harness.add(node, Ledger::Seq{2}, Ledger::ID{2})); harness.clock().advance(1s); - BEAST_EXPECT(AddOutcome::current == harness.add(mama, Seq{3}, ID{3})); + BEAST_EXPECT(AddOutcome::current == + harness.add(mama, Ledger::Seq{3}, Ledger::ID{3})); { // Allow slippage that treats all trusted as the current ledger auto res = harness.vals().currentTrustedDistribution( - ID{2}, // Current ledger - ID{1}, // Prior ledger - Seq{0}); // No cutoff + Ledger::ID{2}, // Current ledger + Ledger::ID{1}, // Prior ledger + Ledger::Seq{0}); // No cutoff BEAST_EXPECT(res.size() == 1); - BEAST_EXPECT(res[ID{2}] == 3); + BEAST_EXPECT(res[Ledger::ID{2}] == 3); + BEAST_EXPECT( + getPreferredLedger(Ledger::ID{2}, res) == Ledger::ID{2}); } { // Don't allow slippage back for prior ledger auto res = harness.vals().currentTrustedDistribution( - ID{2}, // Current ledger - ID{0}, // No prior ledger - Seq{0}); // No cutoff + Ledger::ID{2}, // Current ledger + Ledger::ID{0}, // No prior ledger + Ledger::Seq{0}); // No cutoff BEAST_EXPECT(res.size() == 2); - BEAST_EXPECT(res[ID{2}] == 2); - BEAST_EXPECT(res[ID{1}] == 1); + BEAST_EXPECT(res[Ledger::ID{2}] == 2); + BEAST_EXPECT(res[Ledger::ID{1}] == 1); + BEAST_EXPECT( + getPreferredLedger(Ledger::ID{2}, res) == Ledger::ID{2}); } { // Don't allow any slips auto res = harness.vals().currentTrustedDistribution( - ID{0}, // No current ledger - ID{0}, // No prior ledger - Seq{0}); // No cutoff + Ledger::ID{0}, // No current ledger + Ledger::ID{0}, // No prior ledger + Ledger::Seq{0}); // No cutoff BEAST_EXPECT(res.size() == 3); - BEAST_EXPECT(res[ID{1}] == 1); - BEAST_EXPECT(res[ID{2}] == 1); - BEAST_EXPECT(res[ID{3}] == 1); + BEAST_EXPECT(res[Ledger::ID{1}] == 1); + BEAST_EXPECT(res[Ledger::ID{2}] == 1); + BEAST_EXPECT(res[Ledger::ID{3}] == 1); + BEAST_EXPECT( + getPreferredLedger(Ledger::ID{0}, res) == Ledger::ID{3}); } { // Cutoff old sequence numbers auto res = harness.vals().currentTrustedDistribution( - ID{2}, // current ledger - ID{1}, // prior ledger - Seq{2}); // Only sequence 2 or later + Ledger::ID{2}, // current ledger + Ledger::ID{1}, // prior ledger + Ledger::Seq{2}); // Only sequence 2 or later BEAST_EXPECT(res.size() == 1); - BEAST_EXPECT(res[ID{2}] == 2); + BEAST_EXPECT(res[Ledger::ID{2}] == 2); + BEAST_EXPECT( + getPreferredLedger(Ledger::ID{2}, res) == Ledger::ID{2}); } } @@ -806,7 +644,7 @@ class Validations_test : public beast::unit_test::suite b.setLoadFee(1); c.setLoadFee(12); - hash_map> trustedValidations; + hash_map> trustedValidations; //---------------------------------------------------------------------- // checkers @@ -820,11 +658,9 @@ class Validations_test : public beast::unit_test::suite auto const& id = it.first; auto const& expectedValidations = it.second; - BEAST_EXPECT( - harness.vals().numTrustedForLedger(id) == + BEAST_EXPECT(harness.vals().numTrustedForLedger(id) == expectedValidations.size()); - BEAST_EXPECT( - sorted(harness.vals().getTrustedForLedger(id)) == + BEAST_EXPECT(sorted(harness.vals().getTrustedForLedger(id)) == sorted(expectedValidations)); std::vector expectedTimes; @@ -836,30 +672,28 @@ class Validations_test : public beast::unit_test::suite expectedFees.push_back(val.loadFee().value_or(baseFee)); } - BEAST_EXPECT( - sorted(harness.vals().fees(id, baseFee)) == + BEAST_EXPECT(sorted(harness.vals().fees(id, baseFee)) == sorted(expectedFees)); - BEAST_EXPECT( - sorted(harness.vals().getTrustedValidationTimes(id)) == - sorted(expectedTimes)); + BEAST_EXPECT(sorted(harness.vals().getTrustedValidationTimes( + id)) == sorted(expectedTimes)); } }; //---------------------------------------------------------------------- // Add a dummy ID to cover unknown ledger identifiers - trustedValidations[ID{100}] = {}; + trustedValidations[Ledger::ID{100}] = {}; // first round a,b,c agree, d differs for (auto const& node : {a, b, c}) { - auto const val = node.validation(Seq{1}, ID{1}); + auto const val = node.validation(Ledger::Seq{1}, Ledger::ID{1}); BEAST_EXPECT(AddOutcome::current == harness.add(node, val)); if (val.trusted()) trustedValidations[val.ledgerID()].emplace_back(val); } { - auto const val = d.validation(Seq{1}, ID{11}); + auto const val = d.validation(Ledger::Seq{1}, Ledger::ID{11}); BEAST_EXPECT(AddOutcome::current == harness.add(d, val)); trustedValidations[val.ledgerID()].emplace_back(val); } @@ -868,13 +702,13 @@ class Validations_test : public beast::unit_test::suite // second round, a,b,c move to ledger 2, d now thinks ledger 1 for (auto const& node : {a, b, c}) { - auto const val = node.validation(Seq{2}, ID{2}); + auto const val = node.validation(Ledger::Seq{2}, Ledger::ID{2}); BEAST_EXPECT(AddOutcome::current == harness.add(node, val)); if (val.trusted()) trustedValidations[val.ledgerID()].emplace_back(val); } { - auto const val = d.validation(Seq{2}, ID{1}); + auto const val = d.validation(Ledger::Seq{2}, Ledger::ID{1}); BEAST_EXPECT(AddOutcome::current == harness.add(d, val)); trustedValidations[val.ledgerID()].emplace_back(val); } @@ -890,11 +724,12 @@ class Validations_test : public beast::unit_test::suite TestHarness harness; Node a = harness.makeNode(); - BEAST_EXPECT(AddOutcome::current == harness.add(a, Seq{1}, ID{1})); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{1})); + BEAST_EXPECT(AddOutcome::current == + harness.add(a, Ledger::Seq{1}, Ledger::ID{1})); + BEAST_EXPECT(harness.vals().numTrustedForLedger(Ledger::ID{1})); harness.clock().advance(harness.parms().validationSET_EXPIRES); harness.vals().expire(); - BEAST_EXPECT(!harness.vals().numTrustedForLedger(ID{1})); + BEAST_EXPECT(!harness.vals().numTrustedForLedger(Ledger::ID{1})); } void @@ -909,26 +744,22 @@ class Validations_test : public beast::unit_test::suite c = harness.makeNode(); c.untrust(); - hash_map expected; - Validation staleA; + hash_map expected; for (auto const& node : {a, b, c}) { - auto const val = node.validation(Seq{1}, ID{1}); + auto const val = node.validation(Ledger::Seq{1}, Ledger::ID{1}); BEAST_EXPECT(AddOutcome::current == harness.add(node, val)); - if (node.nodeID() == a.nodeID()) - { - staleA = val; - } - else - expected[node.masterKey()] = val; + expected.emplace(node.masterKey(), val); } + Validation staleA = expected.find(a.masterKey())->second; + // Send in a new validation for a, saving the new one into the expected // map after setting the proper prior ledger ID it replaced harness.clock().advance(1s); - auto newVal = a.validation(Seq{2}, ID{2}); + auto newVal = a.validation(Ledger::Seq{2}, Ledger::ID{2}); BEAST_EXPECT(AddOutcome::current == harness.add(a, newVal)); - expected[a.masterKey()] = newVal; + expected.find(a.masterKey())->second = newVal; // Now flush harness.vals().flush(); @@ -943,6 +774,57 @@ class Validations_test : public beast::unit_test::suite BEAST_EXPECT(flushed == expected); } + void + testGetPreferredLedger() + { + using Distribution = hash_map; + + { + Ledger::ID const current{1}; + Distribution dist; + BEAST_EXPECT(getPreferredLedger(current, dist) == current); + } + + { + Ledger::ID const current{1}; + Distribution dist; + dist[Ledger::ID{2}] = 2; + BEAST_EXPECT(getPreferredLedger(current, dist) == Ledger::ID{2}); + } + + { + Ledger::ID const current{1}; + Distribution dist; + dist[Ledger::ID{1}] = 1; + dist[Ledger::ID{2}] = 2; + BEAST_EXPECT(getPreferredLedger(current, dist) == Ledger::ID{2}); + } + + { + Ledger::ID const current{1}; + Distribution dist; + dist[Ledger::ID{1}] = 2; + dist[Ledger::ID{2}] = 2; + BEAST_EXPECT(getPreferredLedger(current, dist) == current); + } + + { + Ledger::ID const current{2}; + Distribution dist; + dist[Ledger::ID{1}] = 2; + dist[Ledger::ID{2}] = 2; + BEAST_EXPECT(getPreferredLedger(current, dist) == current); + } + + { + Ledger::ID const current{1}; + Distribution dist; + dist[Ledger::ID{2}] = 2; + dist[Ledger::ID{3}] = 2; + BEAST_EXPECT(getPreferredLedger(current, dist) == Ledger::ID{3}); + } + } + void run() override { @@ -955,9 +837,11 @@ class Validations_test : public beast::unit_test::suite testTrustedByLedgerFunctions(); testExpire(); testFlush(); + testGetPreferredLedger(); } }; BEAST_DEFINE_TESTSUITE(Validations, consensus, ripple); -} // namespace test -} // namespace ripple +} // csf +} // test +} // ripple diff --git a/src/test/csf.h b/src/test/csf.h index 52c7ccb361d..d2a1ef1272c 100644 --- a/src/test/csf.h +++ b/src/test/csf.h @@ -17,10 +17,20 @@ */ //============================================================================== -#include -#include #include +#include #include -#include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/test/csf/BasicNetwork.h b/src/test/csf/BasicNetwork.h index 70096f76b64..6b8ee2089f2 100644 --- a/src/test/csf/BasicNetwork.h +++ b/src/test/csf/BasicNetwork.h @@ -20,24 +20,8 @@ #ifndef RIPPLE_TEST_CSF_BASICNETWORK_H_INCLUDED #define RIPPLE_TEST_CSF_BASICNETWORK_H_INCLUDED -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include namespace ripple { namespace test { @@ -64,16 +48,18 @@ namespace csf { 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 associated connection are discarded. + at either end of the connection will not be delivered. - A timer may be set for a Peer. When the timer expires, - a caller provided lambda is invoked. Timers may be canceled - using a token returned when the timer is created. + When creating the Peer set, the caller needs to provide a + Scheduler object for managing the 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. - After creating the Peer set, constructing the network, - and establishing connections, the caller uses one or more - of the step, step_one, step_for, step_until and step_while - functions to iterate the network, + The graph of peers and connections is internally represented + using Digraph. Clients have + const access to that graph to perform additional operations not + directly provided by BasicNetwork. Peer Requirements: @@ -91,198 +77,40 @@ namespace csf { u < v bool LessThanComparable std::hash

class std::hash is defined for P ! u bool true if u is not-a-peer + */ template class BasicNetwork { -public: using peer_type = Peer; - using clock_type = beast::manual_clock; + using clock_type = Scheduler::clock_type; using duration = typename clock_type::duration; using time_point = typename clock_type::time_point; -private: - struct by_to_tag - { - }; - struct by_from_tag - { - }; - struct by_when_tag - { - }; - - using by_to_hook = boost::intrusive::list_base_hook< - boost::intrusive::link_mode, - boost::intrusive::tag>; - - using by_from_hook = boost::intrusive::list_base_hook< - boost::intrusive::link_mode, - boost::intrusive::tag>; - - using by_when_hook = boost::intrusive::set_base_hook< - boost::intrusive::link_mode>; - - struct msg : by_to_hook, by_from_hook, by_when_hook - { - Peer to; - Peer from; - time_point when; - - msg(msg const&) = delete; - msg& - operator=(msg const&) = delete; - virtual ~msg() = default; - virtual void - operator()() const = 0; - - msg(Peer const& from_, Peer const& to_, time_point when_) - : to(to_), from(from_), when(when_) - { - } - - bool - operator<(msg const& other) const - { - return when < other.when; - } - }; - - template - class msg_impl : public msg - { - private: - Handler const h_; - - public: - msg_impl(msg_impl const&) = delete; - msg_impl& - operator=(msg_impl const&) = delete; - - msg_impl( - Peer const& from_, - Peer const& to_, - time_point when_, - Handler&& h) - : msg(from_, to_, when_), h_(std::move(h)) - { - } - - msg_impl( - Peer const& from_, - Peer const& to_, - time_point when_, - Handler const& h) - : msg(from_, to_, when_), h_(h) - { - } - - void - operator()() const override - { - h_(); - } - }; - - class queue_type - { - private: - using by_to_list = typename boost::intrusive::make_list< - msg, - boost::intrusive::base_hook, - boost::intrusive::constant_time_size>::type; - - using by_from_list = typename boost::intrusive::make_list< - msg, - boost::intrusive::base_hook, - boost::intrusive::constant_time_size>::type; - - using by_when_set = typename boost::intrusive::make_multiset< - msg, - boost::intrusive::constant_time_size>::type; - - qalloc alloc_; - by_when_set by_when_; - std::unordered_map by_to_; - std::unordered_map by_from_; - - public: - using iterator = typename by_when_set::iterator; - - queue_type(queue_type const&) = delete; - queue_type& - operator=(queue_type const&) = delete; - - explicit queue_type(qalloc const& alloc); - - ~queue_type(); - - bool - empty() const; - - iterator - begin(); - - iterator - end(); - - template - typename by_when_set::iterator - emplace(Peer const& from, Peer const& to, time_point when, Handler&& h); - - void - erase(iterator iter); - - void - remove(Peer const& from, Peer const& to); - }; - struct link_type { - bool inbound; - duration delay; - - link_type(bool inbound_, duration delay_) - : inbound(inbound_), delay(delay_) + bool inbound = false; + duration delay{}; + time_point established{}; + link_type() = default; + link_type(bool inbound_, duration delay_, time_point established_) + : inbound(inbound_), delay(delay_), established(established_) { } }; - using links_type = boost::container::flat_map; - - class link_transform; - - qalloc alloc_; - queue_type queue_; - // VFALCO This is an ugly wart, aged containers - // want a non-const reference to a clock. - clock_type mutable clock_; - std::unordered_map links_; + Scheduler& scheduler; + Digraph links_; public: BasicNetwork(BasicNetwork const&) = delete; BasicNetwork& operator=(BasicNetwork const&) = delete; - BasicNetwork(); - - /** Return the allocator. */ - qalloc const& - alloc() const; - - /** Return the clock. */ - clock_type& - clock() const; - - /** Return the current network time. - - @note The epoch is unspecified - */ - time_point - now() const; + BasicNetwork(Scheduler& s); /** Connect two peers. @@ -327,13 +155,6 @@ class BasicNetwork bool disconnect(Peer const& peer1, Peer const& peer2); - /** Return the range of active links. - - @return A random access range. - */ - boost::transformed_range - links(Peer const& from); - /** Send a message to a peer. Preconditions: @@ -356,335 +177,33 @@ class BasicNetwork void send(Peer const& from, Peer const& to, Function&& f); - // Used to cancel timers - struct cancel_token; - - /** Deliver a timer notification. - - Effects: - - When the network time is reached, - the function will be called with - no arguments. - */ - template - cancel_token - timer(time_point const& when, Function&& f); - - /** Deliver a timer notification. - - Effects: - - When the specified time has elapsed, - the function will be called with - no arguments. - */ - template - cancel_token - timer(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(cancel_token const& token); - - /** Perform breadth-first search. - - Function will be called with this signature: - - void(std::size_t, Peer&); - - The second argument is the distance of the - peer from the start peer, in hops. - */ - template - void - bfs(Peer const& start, Function&& f); - - /** Run the network for up to one message. - - Effects: - - The clock is advanced to the time - of the last delivered message. - - @return `true` if a message was processed. - */ - bool - step_one(); - - /** Run the network until no messages remain. - - Effects: - - The clock is advanced to the time - of the last delivered message. - - @return `true` if any message was processed. - */ - bool - step(); - - /** Run the network while a condition is true. - - Function takes no arguments and will be called - repeatedly after each message is processed to - decide whether to continue. - - Effects: - - The clock is advanced to the time - of the last delivered message. - @return `true` if any message was processed. - */ - template - bool - step_while(Function&& func); - - /** Run the network until the specified time. - - Effects: - - The clock is advanced to the - specified time. - - @return `true` if any messages remain. - */ - bool - step_until(time_point const& until); - - /** Run the network until time has elapsed. - - Effects: - - The clock is advanced by the - specified duration. + /** Return the range of active links. - @return `true` if any messages remain. + @return A random access range over Digraph::Edge instances */ - template - bool - step_for(std::chrono::duration const& amount); -}; - -//------------------------------------------------------------------------------ - -template -BasicNetwork::queue_type::queue_type(qalloc const& alloc) - : alloc_(alloc) -{ -} - -template -BasicNetwork::queue_type::~queue_type() -{ - for (auto iter = by_when_.begin(); iter != by_when_.end();) + auto + links(Peer const& from) { - auto m = &*iter; - ++iter; - m->~msg(); - alloc_.dealloc(m, 1); + return links_.outEdges(from); } -} -template -inline bool -BasicNetwork::queue_type::empty() const -{ - return by_when_.empty(); -} - -template -inline auto -BasicNetwork::queue_type::begin() -> iterator -{ - return by_when_.begin(); -} - -template -inline auto -BasicNetwork::queue_type::end() -> iterator -{ - return by_when_.end(); -} - -template -template -auto -BasicNetwork::queue_type::emplace( - Peer const& from, - Peer const& to, - time_point when, - Handler&& h) -> typename by_when_set::iterator -{ - using msg_type = msg_impl>; - auto const p = alloc_.alloc(1); - auto& m = *new (p) msg_type(from, to, when, std::forward(h)); - if (to) - by_to_[to].push_back(m); - if (from) - by_from_[from].push_back(m); - return by_when_.insert(m); -} - -template -void -BasicNetwork::queue_type::erase(iterator iter) -{ - auto& m = *iter; - if (iter->to) - { - auto& list = by_to_[iter->to]; - list.erase(list.iterator_to(m)); - } - if (iter->from) - { - auto& list = by_from_[iter->from]; - list.erase(list.iterator_to(m)); - } - by_when_.erase(iter); - m.~msg(); - alloc_.dealloc(&m, 1); -} - -template -void -BasicNetwork::queue_type::remove(Peer const& from, Peer const& to) -{ - { - auto& list = by_to_[to]; - for (auto iter = list.begin(); iter != list.end();) - { - auto& m = *iter++; - if (m.from == from) - erase(by_when_.iterator_to(m)); - } - } - { - auto& list = by_to_[from]; - for (auto iter = list.begin(); iter != list.end();) - { - auto& m = *iter++; - if (m.from == to) - erase(by_when_.iterator_to(m)); - } - } -} - -//------------------------------------------------------------------------------ - -template -class BasicNetwork::link_transform -{ -private: - BasicNetwork& net_; - Peer from_; - -public: - using argument_type = typename links_type::value_type; - - class result_type - { - public: - Peer to; - bool inbound; - - result_type(result_type const&) = default; - - result_type( - BasicNetwork& net, - Peer const& from, - Peer const& to_, - bool inbound_) - : to(to_), inbound(inbound_), net_(net), from_(from) - { - } - - /** Disconnect this link. - - Effects: - - The connection is removed at both ends. - */ - bool - disconnect() const - { - return net_.disconnect(from_, to); - } - - private: - BasicNetwork& net_; - Peer from_; - }; - - link_transform(BasicNetwork& net, Peer const& from) : net_(net), from_(from) - { - } - - result_type const - operator()(argument_type const& v) const - { - return result_type(net_, from_, v.first, v.second.inbound); - } -}; - -//------------------------------------------------------------------------------ - -template -struct BasicNetwork::cancel_token -{ -private: - typename queue_type::iterator iter_; - -public: - cancel_token() = delete; - cancel_token(cancel_token const&) = default; - cancel_token& - operator=(cancel_token const&) = default; - -private: - friend class BasicNetwork; - cancel_token(typename queue_type::iterator iter) : iter_(iter) + /** Return the underlying digraph + */ + Digraph const & + graph() const { + return links_; } }; - //------------------------------------------------------------------------------ - -template -BasicNetwork::BasicNetwork() : queue_(alloc_) -{ -} - template -inline qalloc const& -BasicNetwork::alloc() const +BasicNetwork::BasicNetwork(Scheduler& s) : scheduler(s) { - return alloc_; } template -inline auto -BasicNetwork::clock() const -> clock_type& -{ - return clock_; -} - -template -inline auto -BasicNetwork::now() const -> time_point -{ - return clock_.now(); -} - -template -bool +inline bool BasicNetwork::connect( Peer const& from, Peer const& to, @@ -692,169 +211,49 @@ BasicNetwork::connect( { if (to == from) return false; - using namespace std; - if (!links_[from].emplace(to, link_type{false, delay}).second) + time_point const now = scheduler.now(); + if(!links_.connect(from, to, link_type{false, delay, now})) return false; - auto const result = links_[to].emplace(from, link_type{true, delay}); + auto const result = links_.connect(to, from, link_type{true, delay, now}); (void)result; - assert(result.second); + assert(result); return true; } template -bool +inline bool BasicNetwork::disconnect(Peer const& peer1, Peer const& peer2) { - if (links_[peer1].erase(peer2) == 0) + if (! links_.disconnect(peer1, peer2)) return false; - auto const n = links_[peer2].erase(peer1); - (void)n; - assert(n); - queue_.remove(peer1, peer2); + bool r = links_.disconnect(peer2, peer1); + (void)r; + assert(r); return true; } -template -inline auto -BasicNetwork::links(Peer const& from) - -> boost::transformed_range -{ - return boost::adaptors::transform( - links_[from], link_transform{*this, from}); -} - template template inline void BasicNetwork::send(Peer const& from, Peer const& to, Function&& f) { - using namespace std; - auto const iter = links_[from].find(to); - queue_.emplace( - from, to, clock_.now() + iter->second.delay, forward(f)); -} - -template -template -inline auto -BasicNetwork::timer(time_point const& when, Function&& f) -> cancel_token -{ - using namespace std; - return queue_.emplace(nullptr, nullptr, when, forward(f)); -} - -template -template -inline auto -BasicNetwork::timer(duration const& delay, Function&& f) -> cancel_token -{ - return timer(clock_.now() + delay, std::forward(f)); -} - -template -inline void -BasicNetwork::cancel(cancel_token const& token) -{ - queue_.erase(token.iter_); -} - -template -bool -BasicNetwork::step_one() -{ - if (queue_.empty()) - return false; - auto const iter = queue_.begin(); - clock_.set(iter->when); - (*iter)(); - queue_.erase(iter); - return true; -} - -template -bool -BasicNetwork::step() -{ - if (!step_one()) - return false; - for (;;) - if (!step_one()) - break; - return true; -} - -template -template -bool -BasicNetwork::step_while(Function&& f) -{ - bool ran = false; - while (f() && step_one()) - ran = true; - return ran; -} - -template -bool -BasicNetwork::step_until(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 - { - step_one(); - iter = queue_.begin(); - } while (iter != queue_.end() && iter->when <= until); - clock_.set(until); - return iter != queue_.end(); -} - -template -template -inline bool -BasicNetwork::step_for(std::chrono::duration const& amount) -{ - return step_until(now() + amount); -} - -template -template -void -BasicNetwork::bfs(Peer const& start, Function&& f) -{ - std::deque> q; - std::unordered_set seen; - q.emplace_back(start, 0); - seen.insert(start); - while (!q.empty()) - { - auto v = q.front(); - q.pop_front(); - f(v.second, v.first); - for (auto const& link : links_[v.first]) - { - auto const& w = link.first; - if (seen.count(w) == 0) - { - q.emplace_back(w, v.second + 1); - seen.insert(w); - } - } - } + 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(f), this ] { + // only process if still connected and connection was + // not broken since the message was sent + auto link = links_.edge(from, to); + if (link && link->established <= sent) + f(); + }); } -} // csf -} // test -} // ripple +} // namespace csf +} // namespace test +} // namespace ripple #endif diff --git a/src/test/csf/BasicNetwork_test.cpp b/src/test/csf/BasicNetwork_test.cpp index 0973b799aab..808c8a9eca0 100644 --- a/src/test/csf/BasicNetwork_test.cpp +++ b/src/test/csf/BasicNetwork_test.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include namespace ripple { @@ -43,20 +44,20 @@ class BasicNetwork_test : public beast::unit_test::suite template void - start(Net& net) + start(csf::Scheduler& scheduler, Net& net) { using namespace std::chrono_literals; - auto t = net.timer(1s, [&] { set.insert(0); }); + auto t = scheduler.in(1s, [&] { set.insert(0); }); if (id == 0) { for (auto const& link : net.links(this)) - net.send(this, link.to, [&, to = link.to ] { + net.send(this, link.target, [&, to = link.target ] { to->receive(net, this, 1); }); } else { - net.cancel(t); + scheduler.cancel(t); } } @@ -69,7 +70,7 @@ class BasicNetwork_test : public beast::unit_test::suite if (m < 5) { for (auto const& link : net.links(this)) - net.send(this, link.to, [&, mm = m, to = link.to ] { + net.send(this, link.target, [&, mm = m, to = link.target ] { to->receive(net, this, mm); }); } @@ -77,29 +78,26 @@ class BasicNetwork_test : public beast::unit_test::suite }; void - run() override + testNetwork() { using namespace std::chrono_literals; std::vector pv; pv.emplace_back(0); pv.emplace_back(1); pv.emplace_back(2); - csf::BasicNetwork net; + csf::Scheduler scheduler; + csf::BasicNetwork 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])); - std::size_t diameter = 0; - net.bfs( - &pv[0], [&](auto d, Peer*) { diameter = std::max(d, diameter); }); - BEAST_EXPECT(diameter == 2); for (auto& peer : pv) - peer.start(net); - BEAST_EXPECT(net.step_for(0s)); - BEAST_EXPECT(net.step_for(1s)); - BEAST_EXPECT(net.step()); - BEAST_EXPECT(!net.step()); - BEAST_EXPECT(!net.step_for(1s)); + peer.start(scheduler, net); + BEAST_EXPECT(scheduler.step_for(0s)); + BEAST_EXPECT(scheduler.step_for(1s)); + BEAST_EXPECT(scheduler.step()); + BEAST_EXPECT(!scheduler.step()); + BEAST_EXPECT(!scheduler.step_for(1s)); net.send(&pv[0], &pv[1], [] {}); net.send(&pv[1], &pv[0], [] {}); BEAST_EXPECT(net.disconnect(&pv[0], &pv[1])); @@ -109,16 +107,46 @@ class BasicNetwork_test : public beast::unit_test::suite auto const links = net.links(&pv[1]); if (links.empty()) break; - BEAST_EXPECT(links[0].disconnect()); + BEAST_EXPECT(net.disconnect(&pv[1], links[0].target)); } BEAST_EXPECT(pv[0].set == std::set({0, 2, 4})); BEAST_EXPECT(pv[1].set == std::set({1, 3})); BEAST_EXPECT(pv[2].set == std::set({2, 4})); - net.timer(0s, [] {}); + } + + void + testDisconnect() + { + using namespace std::chrono_literals; + csf::Scheduler scheduler; + csf::BasicNetwork net(scheduler); + BEAST_EXPECT(net.connect(0, 1, 1s)); + BEAST_EXPECT(net.connect(0, 2, 2s)); + + std::set 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({1})); + } + + void + run() override + { + testNetwork(); + testDisconnect(); + } }; BEAST_DEFINE_TESTSUITE(BasicNetwork, test, ripple); -} // test -} // ripple +} // namespace test +} // namespace ripple diff --git a/src/test/csf/CollectorRef.h b/src/test/csf/CollectorRef.h new file mode 100644 index 00000000000..c9d24026a2d --- /dev/null +++ b/src/test/csf/CollectorRef.h @@ -0,0 +1,346 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_TEST_CSF_COLLECTOREF_H_INCLUDED +#define RIPPLE_TEST_CSF_COLLECTOREF_H_INCLUDED + +#include +#include + +namespace ripple { +namespace test { +namespace csf { + +/** Holds a type-erased reference to an arbitray 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 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 const&) = 0; + + virtual void + on(PeerID node, tp when, Share const&) = 0; + + virtual void + on(PeerID node, tp when, Share const&) = 0; + + virtual void + on(PeerID node, tp when, Share const&) = 0; + + virtual void + on(PeerID node, tp when, Share const&) = 0; + + virtual void + on(PeerID node, tp when, Receive const&) = 0; + + virtual void + on(PeerID node, tp when, Receive const&) = 0; + + virtual void + on(PeerID node, tp when, Receive const&) = 0; + + virtual void + on(PeerID node, tp when, Receive const&) = 0; + + virtual void + on(PeerID node, tp when, Receive const&) = 0; + + virtual void + on(PeerID node, tp when, Relay const&) = 0; + + virtual void + on(PeerID node, tp when, Relay const&) = 0; + + virtual void + on(PeerID node, tp when, Relay const&) = 0; + + virtual void + on(PeerID node, tp when, Relay const&) = 0; + + virtual void + on(PeerID node, tp when, Relay 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 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; + + virtual void + on(PeerID node, tp when, Share const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Share const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Share const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Share const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Share const& e) override + { + t_.on(node, when, e); + } + + void + on(PeerID node, tp when, Receive const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Receive const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Receive const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Receive const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Receive const& e) override + { + t_.on(node, when, e); + } + + void + on(PeerID node, tp when, Relay const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Relay const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Relay const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Relay const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, Relay const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, SubmitTx const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, StartRound const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, CloseLedger const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, AcceptLedger const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, WrongPrevLedger const& e) override + { + t_.on(node, when, e); + } + + virtual void + on(PeerID node, tp when, FullyValidateLedger const& e) override + { + t_.on(node, when, e); + } + }; + + std::unique_ptr impl_; + +public: + template + CollectorRef(T& t) : impl_{new Any(t)} + { + } + + // Non-copyable + CollectorRef(CollectorRef const& c) = delete; + CollectorRef& operator=(CollectorRef& c) = delete; + + CollectorRef(CollectorRef&&) = default; + CollectorRef& operator=(CollectorRef&&) = default; + + template + 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 collectors_; +public: + + template + void add(Collector & collector) + { + collectors_.emplace_back(collector); + } + + template + void + on(PeerID node, SimTime when, E const& e) + { + for (auto & c : collectors_) + { + c.on(node, when, e); + } + } + +}; + +} // namespace csf +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/csf/Digraph.h b/src/test/csf/Digraph.h new file mode 100644 index 00000000000..0eb7a8c7314 --- /dev/null +++ b/src/test/csf/Digraph.h @@ -0,0 +1,253 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission target use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_CSF_DIGRAPH_H_INCLUDED +#define RIPPLE_TEST_CSF_DIGRAPH_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace csf { + +namespace detail { +// Dummy class when no edge data needed for graph +struct NoEdgeData +{ +}; + +} // namespace detail + +/** 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 Digraph +{ + using Links = boost::container::flat_map; + using Graph = boost::container::flat_map; + 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 which is boost::none if no edge exists + + */ + boost::optional + 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 boost::none; + } + + /** 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 + */ + bool + connected(Vertex source, Vertex target) const + { + return edge(source, target) != boost::none; + } + + /** Range over vertices in the graph + + @return A boost transformed range over the vertices with out edges in the + graph + */ + auto + outVertices() const + { + return boost::adaptors::transform( + graph_, + [](typename 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. + */ + auto + outVertices(Vertex source) const + { + auto transform = [](typename 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. + */ + auto + outEdges(Vertex source) const + { + auto transform = [source](typename 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 + */ + 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 invokable T vertexName(Vertex const &) that + returns the name target use for the vertex in the file + T must be be ostream-able + */ + template + void + saveDot(std::ostream & out, VertexName&& vertexName) const + { + out << "digraph {\n"; + for (auto const& vData : graph_) + { + auto const fromName = vertexName(vData.first); + for (auto const& eData : vData.second) + { + auto const toName = vertexName(eData.first); + out << fromName << " -> " << toName << ";\n"; + } + } + out << "}\n"; + } + + template + void + saveDot(std::string const& fileName, VertexName&& vertexName) const + { + std::ofstream out(fileName); + saveDot(out, std::forward(vertexName)); + } +}; + +} // namespace csf +} // namespace test +} // namespace ripple +#endif diff --git a/src/test/csf/Digraph_test.cpp b/src/test/csf/Digraph_test.cpp new file mode 100644 index 00000000000..c86ba7f9540 --- /dev/null +++ b/src/test/csf/Digraph_test.cpp @@ -0,0 +1,100 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2017 Ripple Labs Inc. + + Permission target use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +class Digraph_test : public beast::unit_test::suite +{ +public: + + void + run() override + { + using namespace csf; + using Graph = Digraph; + 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"); + + 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"); + + + BEAST_EXPECT(graph.connect('a', 'c', "tree")); + + { + std::vector> edges; + + for (auto const & edge : graph.outEdges('a')) + { + edges.emplace_back(edge.source, edge.target, edge.data); + } + + std::vector> 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 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 expectedDot = "digraph {\n" + "a -> b;\n" + "a -> c;\n" + "}\n"; + BEAST_EXPECT(ss.str() == expectedDot); + + + } +}; + +BEAST_DEFINE_TESTSUITE(Digraph, test, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/csf/Histogram.h b/src/test/csf/Histogram.h new file mode 100644 index 00000000000..878530c585c --- /dev/null +++ b/src/test/csf/Histogram.h @@ -0,0 +1,131 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_TEST_CSF_HISTOGRAM_H_INCLUDED +#define RIPPLE_TEST_CSF_HISTOGRAM_H_INCLUDED + +#include +#include +#include + +namespace ripple { +namespace test { +namespace 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; + - Divison: T a; std::size_t b; T c = a/b; + + +*/ +template > +class Histogram +{ + // TODO: Consider logarithimic bins around expected median if this becomes + // unscaleable + std::map counts_; + std::size_t samples = 0; +public: + /** Insert an sample */ + void + insert(T const & s) + { + ++counts_[s]; + ++samples; + } + + /** The number of samples */ + std::size_t + size() const + { + return samples; + } + + /** The number of distinct samples (bins) */ + std::size_t + numBins() const + { + return counts_.size(); + } + + /** Minimum observed value */ + T + minValue() const + { + return counts_.empty() ? T{} : counts_.begin()->first; + } + + /** Maximum observed value */ + T + maxValue() const + { + return counts_.empty() ? T{} : counts_.rbegin()->first; + } + + /** Histogram average */ + 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& it : counts_) + { + tmp += it.first * it.second; + } + 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 + */ + T + percentile(float p) const + { + assert(p >= 0 && p <=1); + std::size_t 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 csf +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/csf/Histogram_test.cpp b/src/test/csf/Histogram_test.cpp new file mode 100644 index 00000000000..65abc138fb2 --- /dev/null +++ b/src/test/csf/Histogram_test.cpp @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + + +#include +#include +#include + + +namespace ripple { +namespace test { + +class Histogram_test : public beast::unit_test::suite +{ +public: + void + run() override + { + using namespace csf; + Histogram 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, test, ripple); + +} // test +} // ripple diff --git a/src/test/csf/Ledger.h b/src/test/csf/Ledger.h deleted file mode 100644 index 03d3fef001c..00000000000 --- a/src/test/csf/Ledger.h +++ /dev/null @@ -1,183 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012-2017 Ripple Labs Inc - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== -#ifndef RIPPLE_TEST_CSF_LEDGER_H_INCLUDED -#define RIPPLE_TEST_CSF_LEDGER_H_INCLUDED - -#include -#include -#include - -namespace ripple { -namespace test { -namespace 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 unit testing, each transaction is a - single integer and the ledger is a 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} - .... - - Tx - Integer - TxSet - Set of Tx - Ledger - Set of Tx and sequence number -*/ - -class Ledger -{ -public: - struct ID - { - std::uint32_t seq = 0; - TxSetType txs = TxSetType{}; - - bool - operator==(ID const& o) const - { - return seq == o.seq && txs == o.txs; - } - - bool - operator!=(ID const& o) const - { - return !(*this == o); - } - - bool - operator<(ID const& o) const - { - return std::tie(seq, txs) < std::tie(o.seq, o.txs); - } - }; - - auto const& - id() const - { - return id_; - } - - auto - seq() const - { - return id_.seq; - } - - auto - closeTimeResolution() const - { - return closeTimeResolution_; - } - - auto - closeAgree() const - { - return closeTimeAgree_; - } - - auto - closeTime() const - { - return closeTime_; - } - - auto - parentCloseTime() const - { - return parentCloseTime_; - } - - auto const& - parentID() const - { - return parentID_; - } - - Json::Value - getJson() const - { - Json::Value res(Json::objectValue); - res["seq"] = seq(); - return res; - } - - //! Apply the given transactions to this ledger - Ledger - close( - TxSetType const& txs, - NetClock::duration closeTimeResolution, - NetClock::time_point const& consensusCloseTime, - bool closeTimeAgree) const - { - Ledger res{*this}; - res.id_.txs.insert(txs.begin(), txs.end()); - res.id_.seq = seq() + 1; - res.closeTimeResolution_ = closeTimeResolution; - res.closeTime_ = effCloseTime( - consensusCloseTime, closeTimeResolution, closeTime()); - res.closeTimeAgree_ = closeTimeAgree; - res.parentCloseTime_ = closeTime(); - res.parentID_ = id(); - return res; - } - -private: - //! Unique identifier of ledger is combination of sequence number and id - ID id_; - - //! Bucket resolution used to determine close time - NetClock::duration closeTimeResolution_ = ledgerDefaultTimeResolution; - - //! When the ledger closed - NetClock::time_point closeTime_; - - //! Whether consenssus agreed on the close time - bool closeTimeAgree_ = true; - - //! Parent ledger id - ID parentID_; - - //! Parent ledger close time - NetClock::time_point parentCloseTime_; -}; - -inline std::ostream& -operator<<(std::ostream& o, Ledger::ID const& id) -{ - return o << id.seq << "," << id.txs; -} - -inline std::string -to_string(Ledger::ID const& id) -{ - std::stringstream ss; - ss << id; - return ss.str(); -} - -} // csf -} // test -} // ripple - -#endif diff --git a/src/test/csf/Peer.h b/src/test/csf/Peer.h index f877f38aa7a..fa07dd3dea4 100644 --- a/src/test/csf/Peer.h +++ b/src/test/csf/Peer.h @@ -19,233 +19,424 @@ #ifndef RIPPLE_TEST_CSF_PEER_H_INCLUDED #define RIPPLE_TEST_CSF_PEER_H_INCLUDED +#include +#include +#include #include #include -#include -#include -#include +#include +#include +#include +#include #include -#include +#include +#include +#include namespace ripple { namespace test { namespace csf { -/** Store validations reached by peers */ -struct Validation -{ - PeerID id; - Ledger::ID ledger; - Ledger::ID prevLedger; -}; - namespace bc = boost::container; -class Validations +/** A single peer in the simulation. + + This is the main work-horse of the consensus simulation framework and is + where many other components are integrated. The peer + + - Implements the Callbacks required by Consensus + - Manages trust & network connections with other peers + - Issues events back to the simulation based on its actions for analysis + by Collectors + - Exposes most internal state for forcibly simulating arbitrary scenarios +*/ +struct Peer { - //< Ledgers seen by peers, saved in order received (which should be order - //< created) - bc::flat_map> nodesFromLedger; - bc::flat_map> nodesFromPrevLedger; - bc::flat_map> - childLedgers; - -public: - void - update(Validation const& v) + /** Basic wrapper of a proposed position taken by a peer. + + For real consensus, this would add additional data for serialization + and signing. For simulation, nothing extra is needed. + */ + class Position { - nodesFromLedger[v.ledger].insert(v.id); - if (v.ledger.seq > 0) + public: + Position(Proposal const& p) : proposal_(p) { - nodesFromPrevLedger[v.prevLedger].insert(v.id); - childLedgers[v.prevLedger][v.ledger]++; } - } - //< The number of peers who have validated this ledger - std::size_t - proposersValidated(Ledger::ID const& prevLedger) const - { - auto it = nodesFromLedger.find(prevLedger); - if (it != nodesFromLedger.end()) - return it->second.size(); - return 0; - } + Proposal const& + proposal() const + { + return proposal_; + } - /** The number of peers that are past this ledger, i.e. - they have a newer most recent ledger, but have this ledger - as an ancestor. - */ - std::size_t - proposersFinished(Ledger::ID const& prevLedger) const - { - auto it = nodesFromPrevLedger.find(prevLedger); - if (it != nodesFromPrevLedger.end()) - return it->second.size(); - return 0; - } + Json::Value + getJson() const + { + return proposal_.getJson(); + } + + private: + Proposal proposal_; + }; - /** Returns the ledger starting from prevLedger with the most validations. + + /** Simulated delays in internal peer processing. */ - Ledger::ID - getBestLCL(Ledger::ID const& currLedger, Ledger::ID const& prevLedger) const - { - auto it = childLedgers.find(prevLedger); - if (it != childLedgers.end() && !it->second.empty()) + struct ProcessingDelays + { + //! Delay in consensus calling doAccept to accepting and issuing + //! validation + //! TODO: This should be a function of the number of transactions + std::chrono::milliseconds ledgerAccept{0}; + + //! Delay in processing validations from remote peers + std::chrono::milliseconds recvValidation{0}; + + // Return the receive delay for message type M, default is no delay + // Received delay is the time from receiving the message to actually + // handling it. + template + SimDuration + onReceive(M const&) const { - std::size_t bestCount = 0; - Ledger::ID bestLedger; + return SimDuration{}; + } - for (auto const& b : it->second) - { - auto currCount = b.second; - if (currLedger == b.first) - currCount++; - if (currCount > bestCount) - bestLedger = b.first; - if (currCount == bestCount && currLedger == b.first) - bestLedger = b.first; - } - return bestLedger; + SimDuration + onReceive(Validation const&) const + { + return recvValidation; } - return currLedger; - } -}; + }; -/** Proposal is a position taken in the consensus process and is represented - directly from the generic types. -*/ -using Proposal = ConsensusProposal; -class PeerPosition -{ -public: - PeerPosition(Proposal const & p) - : proposal_(p) + /** Generic Validations policy that simply ignores recently stale validations + */ + class StalePolicy { - } + Peer& p_; - Proposal const& - proposal() const - { - return proposal_; - } + public: + StalePolicy(Peer& p) : p_{p} + { + } - Json::Value - getJson() const - { - return proposal_.getJson(); - } + NetClock::time_point + now() const + { + return p_.now(); + } -private: - Proposal proposal_; -}; + void + onStale(Validation&& v) + { + } + void + flush(hash_map&& remaining) + { + } + }; -/** Represents a single node participating in the consensus process. - It implements the Adaptor requirements of generic Consensus. -*/ -struct Peer -{ + /** Non-locking mutex to avoid locks in generic Validations + */ + struct NotAMutex + { + void + lock() + { + } + + void + unlock() + { + } + }; + + //! Type definitions for generic consensus using Ledger_t = Ledger; using NodeID_t = PeerID; using TxSet_t = TxSet; - using PeerPosition_t = PeerPosition; + using PeerPosition_t = Position; using Result = ConsensusResult; + //! Logging support that prefixes messages with the peer ID + beast::WrappedSink sink; + beast::Journal j; + + //! Generic consensus Consensus consensus; //! Our unique ID PeerID id; + //! Current signing key + PeerKey key; + + //! The oracle that manages unique ledgers + LedgerOracle& oracle; + + //! Scheduler of events + Scheduler& scheduler; + + //! Handle to network for sending messages + BasicNetwork& net; + + //! Handle to Trust graph of network + TrustGraph& trustGraph; + //! openTxs that haven't been closed in a ledger yet TxSetType openTxs; - //! last ledger this peer closed + //! The last ledger closed by this node Ledger lastClosedLedger; - //! Handle to network for sending messages - BasicNetwork& net; + //! Ledgers this node has closed or loaded from the network + hash_map ledgers; - //! UNL of trusted peers - UNL unl; + //! Validations from trusted nodes + Validations validations; + using AddOutcome = + Validations::AddOutcome; - //! Most recent ledger completed by peers - Validations peerValidations; + //! The most recent ledger that has been fully validated by the network from + //! the perspective of this Peer + Ledger fullyValidatedLedger; - // The ledgers, proposals, TxSets and Txs this peer has seen - bc::flat_map ledgers; + //------------------------------------------------------------------------- + // Store most network messages; these could be purged if memory use ever + // becomes problematic //! Map from Ledger::ID to vector of Positions with that ledger //! as the prior ledger - bc::flat_map> peerPositions_; + bc::flat_map> peerPositions; + //! TxSet associated with a TxSet::ID bc::flat_map txSets; + // Ledgers and txSets that we have already attempted to acquire + bc::flat_set acquiringLedgers; + bc::flat_set acquiringTxSets; + + //! The number of ledgers this peer has completed int completedLedgers = 0; + //! The number of ledgers this peer should complete before stopping to run int targetLedgers = std::numeric_limits::max(); - //! Skew samples from the network clock; to be refactored into a - //! clock time once it is provided separately from the network. + //! Skew of time relative to the common scheduler clock std::chrono::seconds clockSkew{0}; - //! Delay in processing validations from remote peers - std::chrono::milliseconds validationDelay{0}; + //! Simulated delays to use for internal processing + ProcessingDelays delays; + + //! Whether to simulate running as validator or a tracking node + bool runAsValidator = true; + + //TODO: Consider removing these two, they are only a convenience for tests + // Number of proposers in the prior round + std::size_t prevProposers = 0; + // Duration of prior round + std::chrono::milliseconds prevRoundTime; - //! Delay in acquiring missing ledger from the network - std::chrono::milliseconds missingLedgerDelay{0}; + // Quorum of validations needed for a ledger to be fully validated + // TODO: Use the logic in ValidatorList to set this dynamically + std::size_t quorum = 0; - bool validating_ = true; - bool proposing_ = true; + // Simulation parameters + ConsensusParms consensusParms; - ConsensusParms parms_; - std::size_t prevProposers_ = 0; - std::chrono::milliseconds prevRoundTime_; + //! The collectors to report events to + CollectorRefs & collectors; - //! All peers start from the default constructed ledger - Peer(PeerID i, BasicNetwork& n, UNL const& u, ConsensusParms p) - : consensus(n.clock(), *this, beast::Journal{}) + /** Constructor + + @param i Unique PeerID + @param s Simulation Scheduler + @param o Simulation Oracle + @param n Simulation network + @param tg Simulation trust graph + @param c Simulation collectors + @param jIn Simulation journal + + */ + Peer( + PeerID i, + Scheduler& s, + LedgerOracle& o, + BasicNetwork& n, + TrustGraph & tg, + CollectorRefs & c, + beast::Journal jIn) + : sink(jIn, "Peer " + to_string(i) + ": ") + , j(sink) + , consensus(s.clock(), *this, j) , id{i} + , key{id, 0} + , oracle{o} + , scheduler{s} , net{n} - , unl(u) - , parms_(p) + , trustGraph(tg) + , validations{ValidationParms{}, s.clock(), j, *this} + , collectors{c} { + // All peers start from the default constructed genesis ledger ledgers[lastClosedLedger.id()] = lastClosedLedger; + + // nodes always trust themselves . . SHOULD THEY? + trustGraph.trust(this, this); + } + + /** Schedule the provided callback in `when` duration, but if + `when` is 0, call immediately + */ + template + void + schedule(std::chrono::nanoseconds when, T&& what) + { + using namespace std::chrono_literals; + + if (when == 0ns) + what(); + else + scheduler.in(when, std::forward(what)); + } + + // Issue a new event to the collectors + template + void + issue(E const & event) + { + // Use the scheduler time and not the peer's (skewed) local time + collectors.on(id, scheduler.now(), event); + } + + //-------------------------------------------------------------------------- + // Trust and Network members + // Methods for modifying and querying the network and trust graphs from + // the perspective of this Peer + + //< Extend trust to a peer + void + trust(Peer & o) + { + trustGraph.trust(this, &o); + } + + //< Revoke trust from a peer + void + untrust(Peer & o) + { + trustGraph.untrust(this, &o); + } + + //< Check whether we trust a peer + bool + trusts(Peer & o) + { + return trustGraph.trusts(this, &o); + } + + //< Check whether we trust a peer based on its ID + bool + trusts(PeerID const & oId) + { + for(auto const & p : trustGraph.trustedPeers(this)) + if(p->id == oId) + return true; + return false; + } + + /** Create network connection + + Creates a new outbound connection to another Peer if none exists + + @param o The peer with the inbound connection + @param dur The fixed delay for messages between the two Peers + @return Whether the connection was created. + */ + + bool + connect(Peer & o, SimDuration dur) + { + return net.connect(this, &o, dur); } + /** Remove a network connection + + Removes a connection between peers if one exists + + @param o The peer we disconnect from + @return Whether the connection was removed + */ + bool + disconnect(Peer & o) + { + return net.disconnect(this, &o); + } + + //-------------------------------------------------------------------------- + // Generic Consensus members + + // Attempt to acquire the Ledger associated with the given ID Ledger const* - acquireLedger(Ledger::ID const& ledgerHash) + acquireLedger(Ledger::ID const& ledgerID) { - auto it = ledgers.find(ledgerHash); + auto it = ledgers.find(ledgerID); if (it != ledgers.end()) return &(it->second); - // TODO Get from network/oracle properly! + // Don't retry if we already are acquiring it + if(!acquiringLedgers.emplace(ledgerID).second) + return nullptr; for (auto const& link : net.links(this)) { - auto const& p = *link.to; - auto it = p.ledgers.find(ledgerHash); - if (it != p.ledgers.end()) - { - schedule( - missingLedgerDelay, - [ this, ledgerHash, ledger = it->second ]() { - ledgers.emplace(ledgerHash, ledger); - }); - if (missingLedgerDelay == 0ms) - return &ledgers[ledgerHash]; - break; - } + // Send a messsage to neighbors to find the ledger + net.send( + this, link.target, [ to = link.target, from = this, ledgerID ]() { + auto it = to->ledgers.find(ledgerID); + if (it != to->ledgers.end()) + { + // if the ledger is found, send it back to the original + // requesting peer where it is added to the available + // ledgers + to->net.send(to, from, [ from, ledger = it->second ]() { + from->ledgers.emplace(ledger.id(), ledger); + }); + } + }); } return nullptr; } + // Attempt to acquire the TxSet associated with the given ID TxSet const* acquireTxSet(TxSet::ID const& setId) { auto it = txSets.find(setId); if (it != txSets.end()) return &(it->second); - // TODO Get from network/oracle instead! + + // Don't retry if we already are acquiring it + if(!acquiringTxSets.emplace(setId).second) + return nullptr; + + for (auto const& link : net.links(this)) + { + // Send a message to neighbors to find the tx set + net.send( + this, link.target, [ to = link.target, from = this, setId ]() { + auto it = to->txSets.find(setId); + if (it != to->txSets.end()) + { + // If the txSet is found, send it back to the original + // requesting peer, where it is handled like a TxSet + // that was broadcast over the network + to->net.send(to, from, [ from, txSet = it->second ]() { + from->handle(txSet); + }); + } + }); + } return nullptr; } @@ -258,27 +449,32 @@ struct Peer std::size_t proposersValidated(Ledger::ID const& prevLedger) { - return peerValidations.proposersValidated(prevLedger); + return validations.numTrustedForLedger(prevLedger); } std::size_t proposersFinished(Ledger::ID const& prevLedger) { - return peerValidations.proposersFinished(prevLedger); + return validations.getNodesAfter(prevLedger); } Result - onClose(Ledger const& prevLedger, NetClock::time_point closeTime, ConsensusMode mode) + onClose( + Ledger const& prevLedger, + NetClock::time_point closeTime, + ConsensusMode mode) { - TxSet res{openTxs}; + issue(CloseLedger{prevLedger, openTxs}); - return Result(TxSet{openTxs}, - Proposal(prevLedger.id(), - Proposal::seqJoin, - res.id(), - closeTime, - now(), - id)); + return Result( + TxSet{openTxs}, + Proposal( + prevLedger.id(), + Proposal::seqJoin, + TxSet::calcID(openTxs), + closeTime, + now(), + id)); } void @@ -288,7 +484,7 @@ struct Peer NetClock::duration const& closeResolution, ConsensusCloseTimes const& rawCloseTimes, ConsensusMode const& mode, - Json::Value && consensusJson) + Json::Value&& consensusJson) { onAccept( result, @@ -306,146 +502,357 @@ struct Peer NetClock::duration const& closeResolution, ConsensusCloseTimes const& rawCloseTimes, ConsensusMode const& mode, - Json::Value && consensusJson) + Json::Value&& consensusJson) + { + schedule(delays.ledgerAccept, [&]() { + TxSet const acceptedTxs = injectTxs(prevLedger, result.set); + Ledger const newLedger = oracle.accept( + prevLedger, + acceptedTxs.txs(), + closeResolution, + result.position.closeTime()); + ledgers[newLedger.id()] = newLedger; + + issue(AcceptLedger{newLedger, lastClosedLedger}); + prevProposers = result.proposers; + prevRoundTime = result.roundTime.read(); + lastClosedLedger = newLedger; + + auto const it = std::remove_if( + openTxs.begin(), openTxs.end(), [&](Tx const& tx) { + return acceptedTxs.exists(tx.id()); + }); + openTxs.erase(it, openTxs.end()); + + // Only send validation if the new ledger is compatible with our + // fully validated ledger + bool const isCompatible = + oracle.isAncestor(fullyValidatedLedger, newLedger); + + if (runAsValidator && isCompatible) + { + Validation v{newLedger.id(), + newLedger.seq(), + now(), + now(), + key, + id, + false}; + // share is not trusted + share(v); + // we trust ourselves + addTrustedValidation(v); + } + + checkFullyValidated(newLedger); + + // kick off the next round... + // in the actual implementation, this passes back through + // network ops + ++completedLedgers; + // startRound sets the LCL state, so we need to call it once after + // the last requested round completes + if (completedLedgers <= targetLedgers) + { + startRound(); + } + }); + } + + // Earliest allowed sequence number when checking for ledgers with more + // validations than our current ledger + Ledger::Seq + earliestAllowedSeq() const { - auto newLedger = prevLedger.close( - result.set.txs_, - closeResolution, - result.position.closeTime(), - result.position.closeTime() != NetClock::time_point{}); - ledgers[newLedger.id()] = newLedger; - prevProposers_ = result.proposers; - prevRoundTime_ = result.roundTime.read(); - lastClosedLedger = newLedger; - - auto it = - std::remove_if(openTxs.begin(), openTxs.end(), [&](Tx const& tx) { - return result.set.exists(tx.id()); - }); - openTxs.erase(it, openTxs.end()); - - if (validating_) - relay(Validation{id, newLedger.id(), newLedger.parentID()}); - - // kick off the next round... - // in the actual implementation, this passes back through - // network ops - ++completedLedgers; - // startRound sets the LCL state, so we need to call it once after - // the last requested round completes - // TODO: reconsider this and instead just save LCL generated here? - if (completedLedgers <= targetLedgers) - { - consensus.startRound( - now(), lastClosedLedger.id(), lastClosedLedger, proposing_); - } + if (lastClosedLedger.seq() > Ledger::Seq{20}) + return lastClosedLedger.seq() - Ledger::Seq{20}; + return Ledger::Seq{0}; } Ledger::ID - getPrevLedger(Ledger::ID const& ledgerID, Ledger const& ledger, ConsensusMode mode) + getPrevLedger( + Ledger::ID const& ledgerID, + Ledger const& ledger, + ConsensusMode mode) { - // TODO: Use generic validation code - if (mode != ConsensusMode::wrongLedger && ledgerID.seq > 0 && - ledger.id().seq > 0) - return peerValidations.getBestLCL(ledgerID, ledger.parentID()); - return ledgerID; + // only do if we are past the genesis ledger + if (ledger.seq() == Ledger::Seq{0}) + return ledgerID; + + Ledger::ID parentID{0}; + // Only set the parent ID if we believe ledger is the right ledger + if (mode != ConsensusMode::wrongLedger) + parentID = ledger.parentID(); + + // Get validators that are on our ledger, or "close" to being on + // our ledger. + auto const ledgerCounts = validations.currentTrustedDistribution( + ledgerID, parentID, earliestAllowedSeq()); + + Ledger::ID const netLgr = getPreferredLedger(ledgerID, ledgerCounts); + + if (netLgr != ledgerID) + { + issue(WrongPrevLedger{ledgerID, netLgr}); + } + return netLgr; } void propose(Proposal const& pos) { - if (proposing_) - relay(PeerPosition(pos)); + share(pos); } - ConsensusParms const & + ConsensusParms const& parms() const { - return parms_; + return consensusParms; } - //------------------------------------------------------------------------- - // non-callback helpers + // Not interested in tracking consensus mode changes for now void - receive(PeerPosition const& peerPos) + onModeChange(ConsensusMode, ConsensusMode) { - Proposal const & p = peerPos.proposal(); - if (unl.find(p.nodeID()) == unl.end()) - return; - - // TODO: Supress repeats more efficiently - auto& dest = peerPositions_[p.prevLedger()]; - if (std::find(dest.begin(), dest.end(), p) != dest.end()) - return; + } - dest.push_back(p); - consensus.peerProposal(now(), peerPos); + // Share a message by broadcasting to all connected peers + template + void + share(M const& m) + { + issue(Share{m}); + send(BroadcastMesg{m,router.nextSeq++, this->id}, this->id); } + // Unwrap the Position and share the raw proposal void - receive(TxSet const& txs) + share(Position const & p) { - // save and map complete? - auto it = txSets.insert(std::make_pair(txs.id(), txs)); - if (it.second) - consensus.gotTxSet(now(), txs); + share(p.proposal()); + } + + //-------------------------------------------------------------------------- + // Validation members + + /** Add a trusted validation and return true if it is worth forwarding */ + bool + addTrustedValidation(Validation v) + { + v.setTrusted(); + v.setSeen(now()); + AddOutcome const res = validations.add(v.key(), v); + + if(res == AddOutcome::stale || res == AddOutcome::repeat) + return false; + + // Acquire will try to get from network if not already local + if (Ledger const* lgr = acquireLedger(v.ledgerID())) + checkFullyValidated(*lgr); + return true; } + /** Check if a new ledger can be deemed fully validated */ void - receive(Tx const& tx) + checkFullyValidated(Ledger const& ledger) { - if (openTxs.find(tx.id()) == openTxs.end()) + // Only consider ledgers newer than our last fully validated ledger + if (ledger.seq() <= fullyValidatedLedger.seq()) + return; + + std::size_t const count = validations.numTrustedForLedger(ledger.id()); + std::size_t const numTrustedPeers = trustGraph.graph().outDegree(this); + quorum = static_cast(std::ceil(numTrustedPeers * 0.8)); + if (count >= quorum) { - openTxs.insert(tx); - // relay to peers??? - relay(tx); + issue(FullyValidateLedger{ledger, fullyValidatedLedger}); + fullyValidatedLedger = ledger; } } + //------------------------------------------------------------------------- + // Peer messaging members + + // Basic Sequence number router + // A message that will be flooded across the network is tagged with a + // seqeuence number by the origin node in a BroadcastMesg. Receivers will + // ignore a message as stale if they've already processed a newer sequence + // number, or will process and potentially relay the message along. + // + // The various bool handle(MessageType) members do the actual processing + // and should return true if the message should continue to be sent to peers. + // + // WARN: This assumes messages are received and processed in the order they + // are sent, so that a peer receives a message with seq 1 from node 0 + // before seq 2 from node 0, etc. + // TODO: Break this out into a class and identify type interface to allow + // alternate routing strategies + template + struct BroadcastMesg + { + M mesg; + std::size_t seq; + PeerID origin; + }; + + struct Router + { + std::size_t nextSeq = 1; + bc::flat_map lastObservedSeq; + }; + + Router router; + + // Send a broadcast message to all peers + template void - receive(Validation const& v) + send(BroadcastMesg const& bm, PeerID from) { - if (unl.find(v.id) != unl.end()) + for (auto const& link : net.links(this)) { - schedule(validationDelay, [&, v]() { peerValidations.update(v); }); + if (link.target->id != from && link.target->id != bm.origin) + { + // cheat and don't bother sending if we know it has already been + // used on the other end + if (link.target->router.lastObservedSeq[bm.origin] < bm.seq) + { + issue(Relay{link.target->id, bm.mesg}); + net.send( + this, link.target, [to = link.target, bm, id = this->id ] { + to->receive(bm, id); + }); + } + } } } - template + // Receive a shared message, process it and consider continuing to relay it + template void - relay(T const& t) + receive(BroadcastMesg const& bm, PeerID from) { - for (auto const& link : net.links(this)) - net.send( - this, link.to, [ msg = t, to = link.to ] { to->receive(msg); }); + issue(Receive{from, bm.mesg}); + if (router.lastObservedSeq[bm.origin] < bm.seq) + { + router.lastObservedSeq[bm.origin] = bm.seq; + schedule(delays.onReceive(bm.mesg), [this, bm, from] + { + if (handle(bm.mesg)) + send(bm, from); + }); + } + } + + // Type specific receive handlers, return true if the message should + // continue to be broadcast to peers + bool + handle(Proposal const& p) + { + // Only relay untrusted proposals on the same ledger + if(!trusts(p.nodeID())) + return p.prevLedger() == lastClosedLedger.id(); + + // TODO: This always suppresses relay of peer positions already seen + // Should it allow forwarding if for a recent ledger ? + auto& dest = peerPositions[p.prevLedger()]; + if (std::find(dest.begin(), dest.end(), p) != dest.end()) + return false; + + dest.push_back(p); + + // Rely on consensus to decide whether to relay + return consensus.peerProposal(now(), Position{p}); } - // Receive and relay locally submitted transaction + bool + handle(TxSet const& txs) + { + auto const it = txSets.insert(std::make_pair(txs.id(), txs)); + if (it.second) + consensus.gotTxSet(now(), txs); + // relay only if new + return it.second; + } + + bool + handle(Tx const& tx) + { + // Ignore and suppress relay of transactions already in last ledger + TxSetType const& lastClosedTxs = lastClosedLedger.txs(); + if (lastClosedTxs.find(tx) != lastClosedTxs.end()) + return false; + + // only relay if it was new to our open ledger + return openTxs.insert(tx).second; + + } + + bool + handle(Validation const& v) + { + // TODO: This is not relaying untrusted validations + if (!trusts(v.nodeID())) + return false; + + // Will only relay if current + return addTrustedValidation(v); + } + + //-------------------------------------------------------------------------- + // A locally submitted transaction void submit(Tx const& tx) { - receive(tx); - relay(tx); + issue(SubmitTx{tx}); + if(handle(tx)) + share(tx); } + //-------------------------------------------------------------------------- + // Simulation "driver" members + + //! Heartbeat timer call void timerEntry() { consensus.timerEntry(now()); // only reschedule if not completed if (completedLedgers < targetLedgers) - net.timer(parms().ledgerGRANULARITY, [&]() { timerEntry(); }); + scheduler.in(parms().ledgerGRANULARITY, [this]() { timerEntry(); }); } + + // Called to begin the next round + void + startRound() + { + auto const valDistribution = validations.currentTrustedDistribution( + lastClosedLedger.id(), + lastClosedLedger.parentID(), + earliestAllowedSeq()); + + // Between rounds, we take the majority ledger and use the + Ledger::ID const bestLCL = + getPreferredLedger(lastClosedLedger.id(), valDistribution); + + issue(StartRound{bestLCL, lastClosedLedger}); + + // TODO: + // - Get dominant peer ledger if no validated available? + // - Check that we are switching to something compatible with our + // (network) validated history of ledgers? + consensus.startRound( + now(), bestLCL, lastClosedLedger, runAsValidator); + } + + // Start the consensus process assuming it is not yet running + // This runs forever unless targetLedgers is specified void start() { - net.timer(parms().ledgerGRANULARITY, [&]() { timerEntry(); }); - // The ID is the one we have seen the most validations for - // In practice, we might not actually have that ledger itself yet, - // so there is no gaurantee that bestLCL == lastClosedLedger.id() - auto bestLCL = peerValidations.getBestLCL( - lastClosedLedger.id(), lastClosedLedger.parentID()); - consensus.startRound(now(), bestLCL, lastClosedLedger, proposing_); + // TODO: Expire validations less frequently? + validations.expire(); + scheduler.in(parms().ledgerGRANULARITY, [&]() { timerEntry(); }); + startRound(); } NetClock::time_point @@ -458,47 +865,51 @@ struct Peer using namespace std::chrono; using namespace std::chrono_literals; return NetClock::time_point(duration_cast( - net.now().time_since_epoch() + 86400s + clockSkew)); + scheduler.now().time_since_epoch() + 86400s + clockSkew)); } - // Schedule the provided callback in `when` duration, but if - // `when` is 0, call immediately - template - void - schedule(std::chrono::nanoseconds when, T&& what) - { - using namespace std::chrono_literals; - - if (when == 0ns) - what(); - else - net.timer(when, std::forward(what)); - } Ledger::ID - prevLedgerID() + prevLedgerID() const { return consensus.prevLedgerID(); } - std::size_t - prevProposers() + //------------------------------------------------------------------------- + // Injects a specific transaction when generating the ledger following + // the provided sequence. This allows simulating a byzantine failure in + // which a node generates the wrong ledger, even when consensus worked + // properly. + // TODO: Make this more robust + hash_map txInjections; + + /** Inject non-consensus Tx + + Injects a transactionsinto the ledger following prevLedger's sequence + number. + + @param prevLedger The ledger we are building the new ledger on top of + @param src The Consensus TxSet + @return Consensus TxSet with inject transactions added if prevLedger.seq + matches a previously registered Tx. + */ + TxSet + injectTxs(Ledger prevLedger, TxSet const & src) { - return prevProposers_; - } + auto const it = txInjections.find(prevLedger.seq()); - std::chrono::milliseconds - prevRoundTime() - { - return prevRoundTime_; - } + if(it == txInjections.end()) + return src; + TxSetType res{src.txs()}; + res.insert(it->second); - // Not interested in tracking consensus mode - void - onModeChange(ConsensusMode, ConsensusMode) {} + return TxSet{res}; + + } }; -} // csf -} // test -} // ripple +} // namespace csf +} // namespace test +} // namespace ripple #endif + diff --git a/src/test/csf/PeerGroup.h b/src/test/csf/PeerGroup.h new file mode 100644 index 00000000000..66ff962577a --- /dev/null +++ b/src/test/csf/PeerGroup.h @@ -0,0 +1,370 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_TEST_CSF_PEERGROUP_H_INCLUDED +#define RIPPLE_TEST_CSF_PEERGROUP_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace 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; + 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(PeerGroup const&) = default; + PeerGroup(Peer* peer) : peers_{1, peer} + { + } + PeerGroup(std::vector&& peers) : peers_{std::move(peers)} + { + std::sort(peers_.begin(), peers_.end()); + } + PeerGroup(std::vector const& peers) : peers_{peers} + { + std::sort(peers_.begin(), peers_.end()); + } + + PeerGroup(std::set const& peers) : peers_{peers.begin(), peers.end()} + { + + } + + iterator + begin() + { + return peers_.begin(); + } + + iterator + end() + { + return peers_.end(); + } + + const_iterator + begin() const + { + return peers_.begin(); + } + + 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::find(peers_.begin(), peers_.end(), p) != peers_.end(); + } + + 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 coonnection 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::set_union( + a.peers_.begin(), + a.peers_.end(), + b.peers_.begin(), + b.peers_.end(), + std::back_inserter(res.peers_)); + return res; + } + + // Set difference of PeerGroups + friend + PeerGroup + operator-(PeerGroup const & a, PeerGroup const & b) + { + PeerGroup res; + + std::set_difference( + a.peers_.begin(), + a.peers_.end(), + b.peers_.begin(), + b.peers_.end(), + 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 +std::vector +randomRankedGroups( + PeerGroup & peers, + std::vector const & ranks, + int numGroups, + RandomNumberDistribution sizeDist, + Generator& g) +{ + assert(peers.size() == ranks.size()); + + std::vector groups; + groups.reserve(numGroups); + std::vector rawPeers(peers.begin(), peers.end()); + std::generate_n(std::back_inserter(groups), numGroups, [&]() { + std::vector res = random_weighted_shuffle(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 +void +randomRankedTrust( + PeerGroup & peers, + std::vector const & ranks, + int numGroups, + RandomNumberDistribution sizeDist, + Generator& g) +{ + std::vector const groups = + randomRankedGroups(peers, ranks, numGroups, sizeDist, g); + + std::uniform_int_distribution u(0, groups.size() - 1); + 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 +void +randomRankedConnect( + PeerGroup & peers, + std::vector const & ranks, + int numGroups, + RandomNumberDistribution sizeDist, + Generator& g, + SimDuration delay) +{ + std::vector const groups = + randomRankedGroups(peers, ranks, numGroups, sizeDist, g); + + std::uniform_int_distribution u(0, groups.size() - 1); + for(auto & peer : peers) + { + for(auto & target : groups[u(g)]) + peer->connect(*target, delay); + } +} + +} // namespace csf +} // namespace test +} // namespace ripple +#endif + diff --git a/src/test/csf/Proposal.h b/src/test/csf/Proposal.h new file mode 100644 index 00000000000..fb8fdbb1448 --- /dev/null +++ b/src/test/csf/Proposal.h @@ -0,0 +1,39 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_TEST_CSF_PROPOSAL_H_INCLUDED +#define RIPPLE_TEST_CSF_PROPOSAL_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace csf { +/** Proposal is a position taken in the consensus process and is represented + directly from the generic types. +*/ +using Proposal = ConsensusProposal; + +} // namespace csf +} // namespace test +} // namespace ripple + +#endif \ No newline at end of file diff --git a/src/test/csf/README.md b/src/test/csf/README.md new file mode 100644 index 00000000000..ff6bdc5dfac --- /dev/null +++ b/src/test/csf/README.md @@ -0,0 +1,191 @@ +# 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 Ripple consensus +algorithm implementation. The framework is in its early stages, so the design +and supported features are subject to change. + +## Overview + +The simulation framework focuses on simulating the core consensus and validation +algorithms as a [discrete event +simulation](https://en.wikipedia.org/wiki/Discrete_event_simulation). It is +completely abstracted from the details of the XRP ledger and transactions. In +the simulation, a ledger is simply a set of observed integers and transactions +are single integers. The consensus process works to agree on the set of integers +to include in the next ledger. + +![CSF Overview](./csf_overview.png "CSF Overview") + +The diagram above gives a stylized overview of the components provided by the +framework. These are combined by the simulation author into the simulation +specification, which defines the configuration of the system and the data to +collect when running the simulation. The specification includes: + +- A collection of [`Peer`s](./Peer.h) that represent the participants in the + network, with each independently running the consensus algorithm. +- The `Peer` trust relationships as a `TrustGraph`. This is a directed graph + whose edges define what other `Peer`s a given `Peer` trusts. In other words, + the set of out edges for a `Peer` in the graph correspond to the UNL of that + `Peer`. +- The network communication layer as a `BasicNetwork`. This models the overlay + network topology in which messages are routed between `Peer`s. This graph + topology can be configured independently from the `TrustGraph`. +- Transaction `Submitter`s that model the submission of client transactions to + the network. +- `Collector`s that aggregate, filter and analyze data from the simulation. + Typically, this is used to monitor invariants or generate reports. + +Once specified, the simulation runs using a single `Scheduler` that manages the +global clock and sequencing of activity. During the course of simulation, +`Peer`s generate `Ledger`s and `Validation`s as a result of consensus, +eventually fully validating the consensus history of accepted transactions. Each +`Peer` also issues various `Event`s during the simulation, which are analyzed by +the registered `Collector`s. + +## Example Simulation +Below is a basic simulation we can walk through to get an understanding of the +framework. This simulation is for a set of 5 validators that aren't directly +connected but rely on a single hub node for communication. + +![Example Sim](./csf_graph.png "Example Sim") + +Each Peer has a unique transaction submitted, then runs one round of the +consensus algorithm. + +```c++ +Sim sim; +PeerGroup validators = sim.createGroup(5); +PeerGroup center = sim.createGroup(1); +PeerGroup network = validators + center; +center[0]->runAsValidator = false; + +validators.trust(validators); +center.trust(validators); + +using namespace std::chrono; +SimDuration delay = 200ms; +validators.connect(center, delay); + +SimDurationCollector simDur; +sim.collectors.add(simDur); + +// prep round to set initial state. +sim.run(1); + +// everyone submits their own ID as a TX and relay it to peers +for (Peer * p : validators) + p->submit(Tx(static_cast(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 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(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. \ No newline at end of file diff --git a/src/test/csf/Scheduler.h b/src/test/csf/Scheduler.h new file mode 100644 index 00000000000..cdd774aad38 --- /dev/null +++ b/src/test/csf/Scheduler.h @@ -0,0 +1,465 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_CSF_SCHEDULER_H_INCLUDED +#define RIPPLE_TEST_CSF_SCHEDULER_H_INCLUDED + +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace 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, step_one, step_for, step_until and + step_while functions to process scheduled events. +*/ +class Scheduler +{ +public: + using clock_type = beast::manual_clock; + + using duration = typename clock_type::duration; + + using time_point = typename clock_type::time_point; + +private: + using by_when_hook = boost::intrusive::set_base_hook< + boost::intrusive::link_mode>; + + 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 event_impl : public event + { + Handler const h_; + + public: + event_impl(event_impl const&) = delete; + + event_impl& + operator=(event_impl const&) = delete; + + template + event_impl(time_point when_, DeducedHandler&& h) + : event(when_) + , h_(std::forward(h)) + { + } + + void + operator()() const override + { + h_(); + } + }; + + class queue_type + { + private: + using by_when_set = typename boost::intrusive::make_multiset< + event, + boost::intrusive::constant_time_size>::type; + + qalloc alloc_; + by_when_set by_when_; + + public: + using iterator = typename by_when_set::iterator; + + queue_type(queue_type const&) = delete; + queue_type& + operator=(queue_type const&) = delete; + + explicit queue_type(qalloc const& alloc); + + ~queue_type(); + + bool + empty() const; + + iterator + begin(); + + iterator + end(); + + template + typename by_when_set::iterator + emplace(time_point when, Handler&& h); + + iterator + erase(iterator iter); + }; + + qalloc alloc_; + queue_type 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 allocator. */ + qalloc const& + alloc() const; + + /** 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 cancel_token; + + /** Schedule an event at a specific time + + Effects: + + When the network time is reached, + the function will be called with + no arguments. + */ + template + cancel_token + 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 + cancel_token + 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(cancel_token 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 + step_one(); + + /** 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 + bool + step_while(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 + step_until(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 + bool + step_for(std::chrono::duration const& amount); +}; + +//------------------------------------------------------------------------------ + +inline Scheduler::queue_type::queue_type(qalloc const& alloc) : alloc_(alloc) +{ +} + +inline Scheduler::queue_type::~queue_type() +{ + for (auto iter = by_when_.begin(); iter != by_when_.end();) + { + auto e = &*iter; + ++iter; + e->~event(); + alloc_.dealloc(e, 1); + } +} + +inline bool +Scheduler::queue_type::empty() const +{ + return by_when_.empty(); +} + +inline auto +Scheduler::queue_type::begin() -> iterator +{ + return by_when_.begin(); +} + +inline auto +Scheduler::queue_type::end() -> iterator +{ + return by_when_.end(); +} + + +template +inline auto +Scheduler::queue_type::emplace(time_point when, Handler&& h) -> + typename by_when_set::iterator +{ + using event_type = event_impl>; + auto const p = alloc_.alloc(1); + auto& e = *new (p) event_type( + when, std::forward(h)); + return by_when_.insert(e); +} + +inline auto +Scheduler::queue_type::erase(iterator iter) -> typename by_when_set::iterator +{ + auto& e = *iter; + auto next = by_when_.erase(iter); + e.~event(); + alloc_.dealloc(&e, 1); + return next; +} + +//----------------------------------------------------------------------------- +struct Scheduler::cancel_token +{ +private: + typename queue_type::iterator iter_; + +public: + cancel_token() = delete; + cancel_token(cancel_token const&) = default; + cancel_token& + operator=(cancel_token const&) = default; + +private: + friend class Scheduler; + cancel_token(typename queue_type::iterator iter) : iter_(iter) + { + } +}; + +//------------------------------------------------------------------------------ +inline Scheduler::Scheduler() : queue_(alloc_) +{ +} + +inline qalloc const& +Scheduler::alloc() const +{ + return alloc_; +} + +inline auto +Scheduler::clock() const -> clock_type & +{ + return clock_; +} + +inline auto +Scheduler::now() const -> time_point +{ + return clock_.now(); +} + +template +inline auto +Scheduler::at(time_point const& when, Function&& f) -> cancel_token +{ + return queue_.emplace(when, std::forward(f)); +} + +template +inline auto +Scheduler::in(duration const& delay, Function&& f) -> cancel_token +{ + return at(clock_.now() + delay, std::forward(f)); +} + +inline void +Scheduler::cancel(cancel_token const& token) +{ + queue_.erase(token.iter_); +} + +inline bool +Scheduler::step_one() +{ + 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 (!step_one()) + return false; + for (;;) + if (!step_one()) + break; + return true; +} + +template +inline bool +Scheduler::step_while(Function&& f) +{ + bool ran = false; + while (f() && step_one()) + ran = true; + return ran; +} + +inline bool +Scheduler::step_until(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 + { + step_one(); + iter = queue_.begin(); + } while (iter != queue_.end() && iter->when <= until); + clock_.set(until); + return iter != queue_.end(); +} + +template +inline bool +Scheduler::step_for(std::chrono::duration const& amount) +{ + return step_until(now() + amount); +} + +} // namespace csf +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/csf/Scheduler_test.cpp b/src/test/csf/Scheduler_test.cpp new file mode 100644 index 00000000000..fd58a744142 --- /dev/null +++ b/src/test/csf/Scheduler_test.cpp @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2017 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +namespace ripple { +namespace test { + +class Scheduler_test : public beast::unit_test::suite +{ +public: + + void + run() override + { + using namespace std::chrono_literals; + csf::Scheduler scheduler; + std::set 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.step_one()); + BEAST_EXPECT(seen == std::set({1})); + BEAST_EXPECT(scheduler.now() == (start + 1s)); + + // No processing if stepping until current time + BEAST_EXPECT(scheduler.step_until(scheduler.now())); + BEAST_EXPECT(seen == std::set({1})); + BEAST_EXPECT(scheduler.now() == (start + 1s)); + + // Process next event + BEAST_EXPECT(scheduler.step_for(1s)); + BEAST_EXPECT(seen == std::set({1,2})); + BEAST_EXPECT(scheduler.now() == (start + 2s)); + + // Don't process cancelled event, but advance clock + scheduler.cancel(token); + BEAST_EXPECT(scheduler.step_for(1s)); + BEAST_EXPECT(seen == std::set({1,2})); + BEAST_EXPECT(scheduler.now() == (start + 3s)); + + // Process until 3 seen ints + BEAST_EXPECT(scheduler.step_while([&]() { return seen.size() < 3; })); + BEAST_EXPECT(seen == std::set({1,2,4})); + BEAST_EXPECT(scheduler.now() == (start + 4s)); + + // Process the rest + BEAST_EXPECT(scheduler.step()); + BEAST_EXPECT(seen == std::set({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({1,2,4,8})); + BEAST_EXPECT(scheduler.now() == (start + 8s)); + } +}; + +BEAST_DEFINE_TESTSUITE(Scheduler, test, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/csf/Sim.h b/src/test/csf/Sim.h index c88ef5e7c0d..23c10d735fd 100644 --- a/src/test/csf/Sim.h +++ b/src/test/csf/Sim.h @@ -20,84 +20,142 @@ #ifndef RIPPLE_TEST_CSF_SIM_H_INCLUDED #define RIPPLE_TEST_CSF_SIM_H_INCLUDED +#include +#include #include -#include +#include +#include +#include +#include +#include + +#include +#include +#include namespace ripple { namespace test { namespace csf { -class Sim +/** Sink that prepends simulation time to messages */ +class BasicSink : public beast::Journal::Sink { + Scheduler::clock_type const & clock_; public: - /** Create a simulator for the given trust graph and network topology. + BasicSink (Scheduler::clock_type const & clock) + : Sink (beast::severities::kDisabled, false) + , clock_{clock} + { + } - Create a simulator for consensus over the given trust graph and connect - the network links between nodes based on the provided topology. + void + write (beast::severities::Severity level, + std::string const& text) override + { + if (level < threshold()) + return; - Topology is is a functor with signature + std::cout << clock_.now().time_since_epoch().count() << " " << text + << std::endl; + } +}; - boost::optional (NodeId i, NodeId j) +class Sim +{ + // Use a deque to have stable pointers even when dynamically adding peers + // - Alternatively consider using unique_ptrs allocated from arena + std::deque peers; - that returns the delay sending messages from node i to node j. +public: + std::mt19937_64 rng; + Scheduler scheduler; + BasicSink sink; + beast::Journal j; + LedgerOracle oracle; + BasicNetwork net; + TrustGraph trustGraph; + CollectorRefs collectors; - In general, this network graph is distinct from the trust graph, but - users can use adaptors to present a TrustGraph as a Topology by - specifying the delay between nodes. + /** Create a simulation - @param g The trust graph between peers. - @param top The network topology between peers. - @param parms Consensus parameters to use in the simulation + Creates a new simulation. The simulation has no peers, no trust links + and no network connections. */ - template - Sim(ConsensusParms parms, TrustGraph const& g, Topology const& top) + Sim() : sink{scheduler.clock()}, j{sink}, net{scheduler} { - peers.reserve(g.numPeers()); - for (int i = 0; i < g.numPeers(); ++i) - peers.emplace_back(i, net, g.unl(i), parms); + } - for (int i = 0; i < peers.size(); ++i) + /** 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 newPeers; + newPeers.reserve(numPeers); + for (std::size_t i = 0; i < numPeers; ++i) { - for (int j = 0; j < peers.size(); ++j) - { - if (i != j) - { - auto d = top(i, j); - if (d) - { - net.connect(&peers[i], &peers[j], *d); - } - } - } + peers.emplace_back( + PeerID{static_cast(peers.size())}, + scheduler, + oracle, + net, + trustGraph, + collectors, + j); + newPeers.emplace_back(&peers.back()); } + return PeerGroup{newPeers}; + } + + //! 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 creates `ledgers` more ledgers. + Has each peer run consensus until it closes `ledgers` more ledgers. - @param ledgers The number of additional ledgers to create + @param ledgers The number of additional ledgers to close */ void - run(int ledgers) - { - for (auto& p : peers) - { - if (p.completedLedgers == 0) - p.relay(Validation{p.id, p.prevLedgerID(), p.prevLedgerID()}); - p.targetLedgers = p.completedLedgers + ledgers; - p.start(); - } - net.step(); - } + run(int ledgers); + + /** Run consensus for the given duration */ + void + run(SimDuration const& dur); + + /** Check whether all peers in the network are synchronized. + + Nodes in the network are synchronized if they share the same last + fully validated and last generated ledger. + */ + bool + synchronized() const; + + /** Calculate the number of branches in the network. + + A branch occurs if two peers have fullyValidatedLedgers that are not on + the same chain of ledgers. + */ + std::size_t + branches() const; - std::vector peers; - BasicNetwork net; }; -} // csf -} // test -} // ripple +} // namespace csf +} // namespace test +} // namespace ripple #endif diff --git a/src/test/csf/SimTime.h b/src/test/csf/SimTime.h new file mode 100644 index 00000000000..2e18a5f9ee5 --- /dev/null +++ b/src/test/csf/SimTime.h @@ -0,0 +1,42 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_CSF_SIMTIME_H_INCLUDED +#define RIPPLE_TEST_CSF_SIMTIME_H_INCLUDED + +#include +#include + +namespace ripple { +namespace test { +namespace csf { + +using RealClock = std::chrono::system_clock; +using RealDuration = RealClock::duration; +using RealTime = RealClock::time_point; + +using SimClock = beast::manual_clock; +using SimDuration = typename SimClock::duration; +using SimTime = typename SimClock::time_point; + +} // namespace csf +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/csf/TrustGraph.h b/src/test/csf/TrustGraph.h new file mode 100644 index 00000000000..6e24befd7e3 --- /dev/null +++ b/src/test/csf/TrustGraph.h @@ -0,0 +1,176 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_CSF_UNL_H_INCLUDED +#define RIPPLE_TEST_CSF_UNL_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace 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 TrustGraph +{ + using Graph = Digraph; + + 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 + 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 + */ + 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 unlA; + std::set unlB; + int overlap; + double required; + }; + + //< Return nodes that fail the white-paper no-forking condition + std::vector + 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; + std::set unique; + for (Peer const & peer : graph_.outVertices()) + { + unique.emplace( + std::begin(trustedPeers(peer)), std::end(trustedPeers(peer))); + } + + std::vector uniqueUNLs(unique.begin(), unique.end()); + std::vector 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 rhs = + 2.0 * (1. - quorum) * std::max(unlA.size(), unlB.size()); + + int 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 + */ + bool + canFork(double quorum) const + { + return !forkablePairs(quorum).empty(); + } +}; + +} // csf +} // test +} // ripple + +#endif diff --git a/src/test/csf/Tx.h b/src/test/csf/Tx.h index cb33b65522d..da68ba40cdd 100644 --- a/src/test/csf/Tx.h +++ b/src/test/csf/Tx.h @@ -18,8 +18,9 @@ //============================================================================== #ifndef RIPPLE_TEST_CSF_TX_H_INCLUDED #define RIPPLE_TEST_CSF_TX_H_INCLUDED - +#include #include +#include #include #include #include @@ -62,32 +63,53 @@ class Tx }; //!------------------------------------------------------------------------- -//! All sets of Tx are represented as a flat_set. +//! All sets of Tx are represented as a flat_set for performance. using TxSetType = boost::container::flat_set; //! TxSet is a set of transactions to consider including in the ledger class TxSet { public: - using ID = TxSetType; + using ID = beast::uhash<>::result_type; using Tx = csf::Tx; - using MutableTxSet = TxSet; - TxSet() = default; - TxSet(TxSetType const& s) : txs_{s} + static ID calcID(TxSetType const & txs) { + return beast::uhash<>{}(txs); } - bool - insert(Tx const& t) + 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 const& s) : txs_{s}, id_{calcID(txs_)} { - return txs_.insert(t).second; } - bool - erase(Tx::ID const& txId) + TxSet(MutableTxSet && m) + : txs_{std::move(m.txs_)}, id_{calcID(txs_)} { - return txs_.erase(Tx{txId}) > 0; } bool @@ -106,12 +128,18 @@ class TxSet return nullptr; } - auto const& - id() const + TxSetType const & + txs() const { return txs_; } + 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 @@ -136,8 +164,12 @@ class TxSet return res; } +private: //! The set contains the actual transactions TxSetType txs_; + + //! The unique ID of this tx set + ID id_; }; //------------------------------------------------------------------------------ diff --git a/src/test/csf/UNL.h b/src/test/csf/UNL.h deleted file mode 100644 index 4f293a860a6..00000000000 --- a/src/test/csf/UNL.h +++ /dev/null @@ -1,258 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012-2017 Ripple Labs Inc - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TEST_CSF_UNL_H_INCLUDED -#define RIPPLE_TEST_CSF_UNL_H_INCLUDED - -#include -#include -#include -#include -#include -#include - -namespace ripple { -namespace test { -namespace 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]/sum_k w[k] -*/ -template -std::vector -random_weighted_shuffle(std::vector v, std::vector 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()); - auto idx = dd(g); - std::swap(v[i], v[idx]); - std::swap(w[i], w[idx]); - } - return v; -} - -/** 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 uf_{0, 1}; - -public: - PowerLawDistribution(double xmin, double a) : xmin_{xmin}, a_{a} - { - inv_ = 1.0 / (1.0 - a_); - } - - template - inline 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_); - } -}; - -//< Unique identifier for each node in the network -using PeerID = std::uint32_t; - -//< A unique node list defines a set of trusted peers used in consensus -using UNL = boost::container::flat_set; - -/** Trust graph defining the consensus simulation - - 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. - - Note that each node implicitly trusts itself but that need not be - explicitly modeled, e.g. UNLS[assignment -*/ -class TrustGraph -{ - //< Unique UNLs for the network - std::vector UNLs_; - - std::vector assignment_; - -public: - //< Constructor - TrustGraph(std::vector UNLs, std::vector assignment) - : UNLs_{UNLs}, assignment_{assignment} - { - } - - //< Whether node `i` trusts node `j` - inline bool - trusts(PeerID i, PeerID j) const - { - return unl(i).find(j) != unl(i).end(); - } - - //< Get the UNL for node `i` - inline UNL const& - unl(PeerID i) const - { - return UNLs_[assignment_[i]]; - } - - //< Check whether this trust graph satisfies the no forking condition - bool - canFork(double quorum) const; - - auto - numPeers() const - { - return assignment_.size(); - } - - //< Save grapviz dot file reprentation of the trust graph - void - save_dot(std::string const& fileName); - - /** Generate a random trust graph based on random ranking of peers - - Generate a random trust graph by - - 1. Randomly ranking the peers acording to RankPDF - 2. Generating `numUNL` random UNLs by sampling without replacement - from the ranked nodes. - 3. Restricting the size of the random UNLs according to SizePDF - - @param size The number of nodes in the trust graph - @param numUNLs The number of UNLs to create - @param rankPDF Generates random positive real numbers to use as ranks - @param unlSizePDF Generates random integeres between (0,size-1) to - restrict the size of generated PDF - @param Generator The uniform random bit generator to use - - @note RankPDF/SizePDF can model the full RandomDistribution concept - defined in the STL, but for the purposes of this function need - only provide: - - auto operator()(Generator & g) - - which should return the random sample. - - - */ - template - static TrustGraph - makeRandomRanked( - int size, - int numUNLs, - RankPDF rankPDF, - SizePDF unlSizePDF, - Generator& g) - { - // 1. Generate ranks - std::vector weights(size); - std::generate( - weights.begin(), weights.end(), [&]() { return rankPDF(g); }); - - // 2. Generate UNLs based on sampling without replacement according - // to weights - std::vector unls(numUNLs); - std::generate(unls.begin(), unls.end(), [&]() { - std::vector ids(size); - std::iota(ids.begin(), ids.end(), 0); - auto res = random_weighted_shuffle(ids, weights, g); - return UNL(res.begin(), res.begin() + unlSizePDF(g)); - }); - - // 3. Assign membership - std::vector assignment(size); - std::uniform_int_distribution u(0, numUNLs - 1); - std::generate( - assignment.begin(), assignment.end(), [&]() { return u(g); }); - - return TrustGraph(unls, assignment); - } - - /** Generate a 2 UNL trust graph with some overlap. - - Generates a trust graph for `size` peers formed from - two cliques with the given overlap. Nodes in the overlap - trust both all other nodes, while nodes outside the overlap - only trust nodes in their clique. - - @param size The number of nodes in the trust graph - @param overlap The number of nodes trusting both cliques - */ - static TrustGraph - makeClique(int size, int overlap); - - /** Generate a complete (fully-connect) trust graph - - Generatest a trust graph in which all peers trust all - other peers. - - @param size The number of nodes in the trust graph - */ - static TrustGraph - makeComplete(int size); -}; - -//< Make the TrustGraph into a topology with delays given by DelayModel -template -auto -topology(TrustGraph const& tg, DelayModel const& d) -{ - return [&](PeerID i, PeerID j) { - return tg.trusts(i, j) ? boost::make_optional(d(i, j)) : boost::none; - }; -} - -class fixed -{ - std::chrono::nanoseconds d_; - -public: - fixed(std::chrono::nanoseconds const& d) : d_{d} - { - } - - inline std::chrono::nanoseconds - operator()(PeerID const& i, PeerID const& j) const - { - return d_; - } -}; - -} // csf -} // test -} // ripple - -#endif diff --git a/src/test/csf/Validation.h b/src/test/csf/Validation.h new file mode 100644 index 00000000000..ab45e4878c3 --- /dev/null +++ b/src/test/csf/Validation.h @@ -0,0 +1,167 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_TEST_CSF_VALIDATION_H_INCLUDED +#define RIPPLE_TEST_CSF_VALIDATION_H_INCLUDED + +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace csf { + + +struct PeerIDTag; +//< Uniquely identifies a peer +using PeerID = tagged_integer; + +/** 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; + +/** 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_ = true; + boost::optional loadFee_; + +public: + Validation(Ledger::ID id, + Ledger::Seq seq, + NetClock::time_point sign, + NetClock::time_point seen, + PeerKey key, + PeerID nodeID, + bool trusted, + boost::optional loadFee = boost::none) + : ledgerID_{id} + , seq_{seq} + , signTime_{sign} + , seenTime_{seen} + , key_{key} + , nodeID_{nodeID} + , trusted_{trusted} + , loadFee_{loadFee} + { + } + + Ledger::ID + ledgerID() const + { + return ledgerID_; + } + + Ledger::Seq + seq() const + { + return seq_; + } + + NetClock::time_point + signTime() const + { + return signTime_; + } + + NetClock::time_point + seenTime() const + { + return seenTime_; + } + + PeerKey + key() const + { + return key_; + } + + PeerID + nodeID() const + { + return nodeID_; + } + + bool + trusted() const + { + return trusted_; + } + + boost::optional + loadFee() const + { + return loadFee_; + } + + Validation const& + unwrap() const + { + return *this; + } + + auto + asTie() const + { + return std::tie(ledgerID_, seq_, signTime_, seenTime_, key_, nodeID_, + trusted_, loadFee_); + } + 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 + setSeen(NetClock::time_point seen) + { + seenTime_ = seen; + } +}; + +} // ripple +} // test +} // csf +#endif \ No newline at end of file diff --git a/src/test/csf/collectors.h b/src/test/csf/collectors.h new file mode 100644 index 00000000000..b340b38d27f --- /dev/null +++ b/src/test/csf/collectors.h @@ -0,0 +1,740 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_TEST_CSF_COLLECTORS_H_INCLUDED +#define RIPPLE_TEST_CSF_COLLECTORS_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace 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 analagous to CollectorRefs + in CollectorRef.h, but does *not* erase the type information of the combined + collectors. + */ +template +class Collectors +{ + std::tuple cs; + + template + static void + apply(C& c, PeerID who, SimTime when, E e) + { + c.on(who, when, e); + } + + template + static void + apply( + std::tuple& cs, + PeerID who, + SimTime when, + E e, + std::index_sequence) + { + // Sean Parent for_each_argument trick (C++ fold expressions would be + // nice here) + (void)std::array{ + {((apply(std::get(cs), who, when, e)), 0)...}}; + } + +public: + /** Constructor + + @param cs References to the collectors to call together + */ + Collectors(Cs&... cs) : cs(std::tie(cs...)) + { + } + + template + void + on(PeerID who, SimTime when, E e) + { + apply(cs, who, when, e, std::index_sequence_for{}); + } +}; + +/** Create an instance of Collectors */ +template +Collectors +makeCollectors(Cs&... cs) +{ + return Collectors(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 +struct CollectByNode +{ + std::map byNode; + + CollectorType& + operator[](PeerID who) + { + return byNode[who]; + } + + CollectorType const& + operator[](PeerID who) const + { + return byNode[who]; + } + template + void + on(PeerID who, SimTime when, E const& e) + { + byNode[who].on(who, when, e); + } + +}; + +/** Collector which ignores all events */ +struct NullCollector +{ + template + void + on(PeerID, SimTime, E const& e) + { + } +}; + +/** Tracks the overall duration of a simulation */ +struct SimDurationCollector +{ + bool init = false; + SimTime start; + SimTime stop; + + template + 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; + boost::optional accepted; + boost::optional validated; + + Tracker(Tx tx_, SimTime submitted_) : tx{tx_}, submitted{submitted_} + { + } + }; + + hash_map txs; + + using Hist = Histogram; + Hist submitToAccept; + Hist submitToValidate; + + // Ignore most events by default + template + 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 + 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 + std::size_t + unvalidated() const + { + return std::count_if(txs.begin(), txs.end(), [](auto const& it) { + return !it.second.validated; + }); + } + + template + 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(simDuration).count(); + }; + + auto fmtS = [](SimDuration dur) + { + return duration_cast>(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 + 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(simDuration).count(); + }; + + auto fmtS = [](SimDuration dur) + { + return duration_cast>(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; + boost::optional fullyValidated; + + Tracker(SimTime accepted_) : accepted{accepted_} + { + } + }; + + hash_map ledgers_; + + using Hist = Histogram; + Hist acceptToFullyValid; + Hist acceptToAccept; + Hist fullyValidToFullyValid; + + // Ignore most events by default + template + 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); + } + } + } + } + } + + std::size_t + unvalidated() const + { + return std::count_if( + ledgers_.begin(), ledgers_.end(), [](auto const& it) { + return !it.second.fullyValidated; + }); + } + + template + 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(simDuration).count(); + }; + + auto fmtS = [](SimDuration dur) + { + return duration_cast>(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 + 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(simDuration).count(); + }; + + auto fmtS = [](SimDuration dur) + { + return duration_cast>(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 + 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 closeJumps; + std::vector fullyValidatedJumps; + + // Ignore most events by default + template + 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{who, when, e.prior, 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{who, when, e.prior, e.ledger}); + } +}; + +} // namespace csf +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/csf/csf_graph.png b/src/test/csf/csf_graph.png new file mode 100644 index 00000000000..4b4e47ed3e8 Binary files /dev/null and b/src/test/csf/csf_graph.png differ diff --git a/src/test/csf/csf_overview.png b/src/test/csf/csf_overview.png new file mode 100644 index 00000000000..82bc1202b80 Binary files /dev/null and b/src/test/csf/csf_overview.png differ diff --git a/src/test/csf/events.h b/src/test/csf/events.h new file mode 100644 index 00000000000..31cfdebb3a4 --- /dev/null +++ b/src/test/csf/events.h @@ -0,0 +1,155 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_TEST_CSF_EVENTS_H_INCLUDED +#define RIPPLE_TEST_CSF_EVENTS_H_INCLUDED + +#include +#include +#include +#include +#include + + +namespace ripple { +namespace test { +namespace csf { + +// Events are emitted by peers at a variety of points during the simulation. +// Each event is emitted by a particlar 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 +// struct Collector +// { +// template +// 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 +struct Share +{ + //! Event that is shared + V val; +}; + +/** A value relayed to another peer as part of flooding + */ +template +struct Relay +{ + //! Peer relaying to + PeerID to; + + //! The value to relay + V val; +}; + +/** A value received from another peer as part of flooding + */ +template +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 csf +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/csf/impl/Sim.cpp b/src/test/csf/impl/Sim.cpp new file mode 100644 index 00000000000..2860744ca62 --- /dev/null +++ b/src/test/csf/impl/Sim.cpp @@ -0,0 +1,76 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include +#include + +namespace ripple { +namespace test { +namespace 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::max(); + p.start(); + } + scheduler.step_for(dur); +} + +bool +Sim::synchronized() const +{ + if (peers.size() < 1) + return true; + Peer const& ref = peers.front(); + return std::all_of(peers.begin(), peers.end(), [&ref](Peer const& p) { + return p.lastClosedLedger.id() == + ref.lastClosedLedger.id() && + p.fullyValidatedLedger.id() == + ref.fullyValidatedLedger.id(); + }); +} + +std::size_t +Sim::branches() const +{ + if(peers.size() < 1) + return 0; + std::set ledgers; + for(auto const & peer : peers) + ledgers.insert(peer.fullyValidatedLedger); + + return oracle.branches(ledgers); +} + +} // namespace csf +} // namespace test +} // namespace ripple diff --git a/src/test/csf/impl/UNL.cpp b/src/test/csf/impl/UNL.cpp deleted file mode 100644 index 7b9207eb322..00000000000 --- a/src/test/csf/impl/UNL.cpp +++ /dev/null @@ -1,131 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012-2017 Ripple Labs Inc - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== -#include -#include -#include -#include - -namespace ripple { -namespace test { -namespace csf { - -bool -TrustGraph::canFork(double quorum) const -{ - // Check the forking condition by looking at intersection - // between all pairs of UNLs. - - // First check if some nodes uses a UNL they are not members of, since - // this creates an implicit UNL with that ndoe. - - auto uniqueUNLs = UNLs_; - - for (int i = 0; i < assignment_.size(); ++i) - { - auto const& myUNL = UNLs_[assignment_[i]]; - if (myUNL.find(i) == myUNL.end()) - { - auto myUNLcopy = myUNL; - myUNLcopy.insert(i); - uniqueUNLs.push_back(std::move(myUNLcopy)); - } - } - - // 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 rhs = - 2.0 * (1. - quorum) * std::max(unlA.size(), unlB.size()); - - int intersectionSize = - std::count_if(unlA.begin(), unlA.end(), [&](PeerID id) { - return unlB.find(id) != unlB.end(); - }); - - if (intersectionSize < rhs) - return true; - } - } - return false; -} - -TrustGraph -TrustGraph::makeClique(int size, int overlap) -{ - using bci = boost::counting_iterator; - - // Split network into two cliques with the given overlap - // Clique A has nodes [0,endA) and Clique B has [startB,numPeers) - // Note: Clique B will have an extra peer when numPeers - overlap - // is odd - int endA = (size + overlap) / 2; - int startB = (size - overlap) / 2; - - std::vector unls; - unls.emplace_back(bci(0), bci(endA)); - unls.emplace_back(bci(startB), bci(size)); - unls.emplace_back(bci(0), bci(size)); - - std::vector assignment(size, 0); - - for (int i = 0; i < size; ++i) - { - if (i < startB) - assignment[i] = 0; - else if (i > endA) - assignment[i] = 1; - else - assignment[i] = 2; - } - - return TrustGraph(unls, assignment); -} - -TrustGraph -TrustGraph::makeComplete(int size) -{ - UNL all{boost::counting_iterator(0), - boost::counting_iterator(size)}; - - return TrustGraph(std::vector(1, all), std::vector(size, 0)); -} - -inline void -TrustGraph::save_dot(std::string const& fileName) -{ - std::ofstream out(fileName); - out << "digraph {\n"; - for (int i = 0; i < assignment_.size(); ++i) - { - for (auto& j : UNLs_[assignment_[i]]) - { - out << i << " -> " << j << ";\n"; - } - } - out << "}\n"; -} - -} // csf -} // test -} // ripple diff --git a/src/test/csf/impl/ledgers.cpp b/src/test/csf/impl/ledgers.cpp new file mode 100644 index 00000000000..eb8e9a7adab --- /dev/null +++ b/src/test/csf/impl/ledgers.cpp @@ -0,0 +1,140 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include +#include + +#include + +namespace ripple { +namespace test { +namespace csf { + +Ledger::Instance const Ledger::genesis; + +Json::Value +Ledger::getJson() const +{ + Json::Value res(Json::objectValue); + res["id"] = static_cast(id()); + res["seq"] = static_cast(seq()); + return res; +} + +LedgerOracle::LedgerOracle() +{ + instances_.insert(InstanceEntry{Ledger::genesis, nextID()}); +} + +Ledger::ID +LedgerOracle::nextID() const +{ + return Ledger::ID{static_cast(instances_.size())}; +} + +Ledger +LedgerOracle::accept( + Ledger const& parent, + TxSetType const& txs, + NetClock::duration closeTimeResolution, + NetClock::time_point const& consensusCloseTime) +{ + 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(); + 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)); +} + +boost::optional +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 boost::none; +} + + +bool +LedgerOracle::isAncestor(Ledger const & ancestor, Ledger const& descendant) const +{ + // The ancestor must have an earlier sequence number than the descendent + if(ancestor.seq() >= descendant.seq()) + return false; + + boost::optional current{descendant}; + while(current && current->seq() > ancestor.seq()) + current = lookup(current->parentID()); + return current && (current->id() == ancestor.id()); +} + +std::size_t +LedgerOracle::branches(std::set const & ledgers) const +{ + // Tips always maintains the Ledgers with largest sequence number + // along all known chains. + std::vector 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 (isAncestor(earlier, later)) + { + tips[idx] = later; + found = true; + } + } + + if(!found) + tips.push_back(ledger); + + } + // The size of tips is the number of branches + return tips.size(); +} +} // namespace csf +} // namespace test +} // namespace ripple diff --git a/src/test/csf/ledgers.h b/src/test/csf/ledgers.h new file mode 100644 index 00000000000..9e48d933144 --- /dev/null +++ b/src/test/csf/ledgers.h @@ -0,0 +1,263 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_TEST_CSF_LEDGERS_H_INCLUDED +#define RIPPLE_TEST_CSF_LEDGERS_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace 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 manges 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 = tagged_integer; + + struct IdTag; + using ID = tagged_integer; + +private: + // The instance is the common immutable data that will be assigned a unique + // ID by the oracle + struct Instance + { + Instance() {} + + // Sequence number + Seq seq{0}; + + // Transactions added to generate this ledger + TxSetType txs; + + // Resolution used to determine close time + NetClock::duration closeTimeResolution = ledgerDefaultTimeResolution; + + //! 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; + + 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 + friend void + hash_append(Hasher& h, Ledger::Instance const& instance) + { + using beast::hash_append; + hash_append(h, instance.asTie()); + } + }; + + + // Single common genesis instance + static const Instance genesis; + + Ledger(ID id, Instance const* i) : id_{id}, instance_{i} + { + } + +public: + Ledger() : id_{0}, instance_(&genesis) + { + } + + ID + id() const + { + return id_; + } + + Seq + seq() const + { + return instance_->seq; + } + + NetClock::duration + closeTimeResolution() const + { + return instance_->closeTimeResolution; + } + + bool + closeAgree() const + { + return instance_->closeTimeAgree; + } + + NetClock::time_point + closeTime() const + { + return instance_->closeTime; + } + + NetClock::time_point + parentCloseTime() const + { + return instance_->parentCloseTime; + } + + ID + parentID() const + { + return instance_->parentID; + } + + TxSetType const& + txs() const + { + return instance_->txs; + } + + 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; + using InstanceEntry = InstanceMap::value_type; + + // Set of all known ledgers; note this is never pruned + InstanceMap instances_; + + // ID for the next unique ledger + Ledger::ID + nextID() const; + +public: + + LedgerOracle(); + + /** Find the ledger with the given ID */ + boost::optional + 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); + + /** Determine whether ancestor is really an ancestor of descendent */ + bool + isAncestor(Ledger const & ancestor, Ledger const& descendant) const; + + /** 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 + */ + std::size_t + branches(std::set const & ledgers) const; + +}; + +} // csf +} // test +} // ripple + +#endif diff --git a/src/test/csf/random.h b/src/test/csf/random.h new file mode 100644 index 00000000000..43519abd2dd --- /dev/null +++ b/src/test/csf/random.h @@ -0,0 +1,178 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_CSF_RANDOM_H_INCLUDED +#define RIPPLE_TEST_CSF_RANDOM_H_INCLUDED + +#include +#include + +namespace ripple { +namespace test { +namespace 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]/sum_k w[k] +*/ +template +std::vector +random_weighted_shuffle(std::vector v, std::vector 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()); + 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 +std::vector +sample( std::size_t size, RandomNumberDistribution dist, Generator& g) +{ + std::vector res(size); + std::generate(res.begin(), res.end(), [&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 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 const& w, + Generator& g) + : first_{first}, last_{last}, dd_{w.begin(), w.end()}, g_{g} + { + using tag = typename std::iterator_traits::iterator_category; + static_assert( + std::is_same::value, + "Selector only supports random access iterators."); + // TODO: Allow for forward iterators + } + + typename std::iterator_traits::value_type + operator()() + { + auto idx = dd_(g_); + return *(first_ + idx); + } +}; + +template +Selector +makeSelector(Iter first, Iter last, std::vector const& w, Generator& g) +{ + return Selector(first, last, w, g); +} + +//------------------------------------------------------------------------------ +// Additional distributions of interest not defined in in + +/** Constant "distribution" that always returns the same value +*/ +class ConstantDistribution +{ + double t_; + +public: + ConstantDistribution(double const& t) : t_{t} + { + } + + template + inline 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 uf_{0, 1}; + +public: + + using result_type = double; + + PowerLawDistribution(double xmin, double a) : xmin_{xmin}, a_{a} + { + inv_ = 1.0 / (1.0 - a_); + } + + template + inline 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_); + } +}; + +} // csf +} // test +} // ripple + +#endif diff --git a/src/test/csf/submitters.h b/src/test/csf/submitters.h new file mode 100644 index 00000000000..de44accf337 --- /dev/null +++ b/src/test/csf/submitters.h @@ -0,0 +1,130 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_TEST_CSF_SUBMITTERS_H_INCLUDED +#define RIPPLE_TEST_CSF_SUBMITTERS_H_INCLUDED + +#include +#include +#include +#include +#include +namespace ripple { +namespace test { +namespace 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; + + double + inv() const + { + return duration.count()/double(count); + } +}; + +/** Submits transactions to a specified peer + + Submits successive transactions beginning at start, then spaced according + to succesive 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 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 + static + std::enable_if_t::value, SimDuration> + asDuration(T t) + { + return SimDuration{static_cast(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 +Submitter +makeSubmitter( + Distribution dist, + SimTime start, + SimTime end, + Selector& sel, + Scheduler& s, + Generator& g) +{ + return Submitter( + dist, start ,end, sel, s, g); +} + +} // namespace csf +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/csf/timers.h b/src/test/csf/timers.h new file mode 100644 index 00000000000..ab9784400c0 --- /dev/null +++ b/src/test/csf/timers.h @@ -0,0 +1,86 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 Ripple Labs Inc + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_TEST_CSF_TIMERS_H_INCLUDED +#define RIPPLE_TEST_CSF_TIMERS_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace 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(60s), + 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 realTime = RealClock::now(); + SimTime simTime = when; + + RealDuration realDuration = realTime - startRealTime_; + SimDuration simDuration = simTime - startSimTime_; + out_ << "Heartbeat. Time Elapsed: {sim: " + << duration_cast(simDuration).count() + << "s | real: " + << duration_cast(realDuration).count() + << "s}\n" << std::flush; + + scheduler_.in(interval_, [this](){beat(scheduler_.now());}); + } +}; + +} // namespace csf +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/unity/consensus_test_unity.cpp b/src/test/unity/consensus_test_unity.cpp index 926add48780..1a9a347a336 100644 --- a/src/test/unity/consensus_test_unity.cpp +++ b/src/test/unity/consensus_test_unity.cpp @@ -17,6 +17,9 @@ */ //============================================================================== +#include #include +#include #include +#include #include diff --git a/src/test/unity/csf_unity.cpp b/src/test/unity/csf_unity.cpp index 248a9b9507a..71bd173cee3 100644 --- a/src/test/unity/csf_unity.cpp +++ b/src/test/unity/csf_unity.cpp @@ -19,5 +19,9 @@ #include +#include +#include #include -#include +#include +#include +#include