From 94c6a2a850081faa006a1e39d30a2536a7f824ba Mon Sep 17 00:00:00 2001 From: Brad Chase Date: Thu, 7 Dec 2017 11:00:42 -0500 Subject: [PATCH] Use LedgerTrie for preferred ledger (RIPD-1551): These changes augment the Validations class with a LedgerTrie to better track the history of support for validated ledgers. This improves the selection of the preferred working ledger for consensus. The Validations class now tracks both full and partial validations. Partial validations are only used to determine the working ledger; full validations are required for any quorum related function. Validators are also now explicitly restricted to sending validations with increasing ledger sequence number. --- Builds/VisualStudio2015/RippleD.vcxproj | 10 + .../VisualStudio2015/RippleD.vcxproj.filters | 9 + docs/source.dox | 1 + src/ripple/app/consensus/RCLConsensus.cpp | 38 +- src/ripple/app/consensus/RCLConsensus.h | 7 +- src/ripple/app/consensus/RCLValidations.cpp | 235 +++-- src/ripple/app/consensus/RCLValidations.h | 140 ++- src/ripple/app/main/Application.cpp | 5 +- src/ripple/app/main/Application.h | 8 +- src/ripple/app/misc/NetworkOPs.cpp | 111 +- src/ripple/consensus/Consensus.h | 9 +- src/ripple/consensus/LedgerTrie.h | 815 +++++++++++++++ src/ripple/consensus/Validations.h | 815 +++++++++------ src/test/app/RCLValidations_test.cpp | 204 ++++ src/test/consensus/Consensus_test.cpp | 169 ++- src/test/consensus/LedgerTrie_test.cpp | 663 ++++++++++++ src/test/consensus/Validations_test.cpp | 964 +++++++++++------- src/test/csf/Peer.h | 159 +-- src/test/csf/PeerGroup.h | 10 +- src/test/csf/Sim.h | 25 +- src/test/csf/Validation.h | 29 +- src/test/csf/impl/Sim.cpp | 31 +- src/test/csf/impl/ledgers.cpp | 65 +- src/test/csf/ledgers.h | 93 +- src/test/unity/app_test_unity2.cpp | 1 + src/test/unity/consensus_test_unity.cpp | 1 + 26 files changed, 3611 insertions(+), 1006 deletions(-) create mode 100644 src/ripple/consensus/LedgerTrie.h create mode 100644 src/test/app/RCLValidations_test.cpp create mode 100644 src/test/consensus/LedgerTrie_test.cpp diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index f682212b208..b3c119e5015 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -1634,6 +1634,8 @@ + + @@ -4501,6 +4503,10 @@ True True + + True + True + True True @@ -4685,6 +4691,10 @@ True True + + True + True + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 7e954e279be..7681fc79bca 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -2151,6 +2151,9 @@ ripple\consensus + + ripple\consensus + ripple\consensus @@ -5268,6 +5271,9 @@ test\app + + test\app + test\app @@ -5406,6 +5412,9 @@ test\consensus + + test\consensus + test\consensus diff --git a/docs/source.dox b/docs/source.dox index d0d82d7a86d..96b1fa73e38 100644 --- a/docs/source.dox +++ b/docs/source.dox @@ -114,6 +114,7 @@ INPUT = \ ../src/ripple/consensus/ConsensusTypes.h \ ../src/ripple/consensus/DisputedTx.h \ ../src/ripple/consensus/LedgerTiming.h \ + ../src/ripple/consensus/LedgerTrie.h \ ../src/ripple/consensus/Validations.h \ ../src/ripple/consensus/ConsensusParms.h \ ../src/ripple/app/consensus/RCLCxTx.h \ diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index 925b701a497..996a3f8bd3d 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -233,9 +233,13 @@ RCLConsensus::Adaptor::proposersValidated(LedgerHash const& h) const } std::size_t -RCLConsensus::Adaptor::proposersFinished(LedgerHash const& h) const +RCLConsensus::Adaptor::proposersFinished( + RCLCxLedger const& ledger, + LedgerHash const& h) const { - return app_.getValidations().getNodesAfter(h); + RCLValidations& vals = app_.getValidations(); + return vals.getNodesAfter( + RCLValidatedLedger(ledger.ledger_, vals.adaptor().journal()), h); } uint256 @@ -244,29 +248,17 @@ RCLConsensus::Adaptor::getPrevLedger( RCLCxLedger const& ledger, ConsensusMode mode) { - uint256 parentID; - // 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. - hash_map ledgerCounts = - app_.getValidations().currentTrustedDistribution( - ledgerID, parentID, ledgerMaster_.getValidLedgerIndex()); - - uint256 netLgr = getPreferredLedger(ledgerID, ledgerCounts); + RCLValidations& vals = app_.getValidations(); + uint256 netLgr = vals.getPreferred( + RCLValidatedLedger{ledger.ledger_, vals.adaptor().journal()}, + ledgerMaster_.getValidLedgerIndex()); if (netLgr != ledgerID) { if (mode != ConsensusMode::wrongLedger) app_.getOPs().consensusViewChange(); - if (auto stream = j_.debug()) - { - for (auto const & it : ledgerCounts) - stream << "V: " << it.first << ", " << it.second; - } + JLOG(j_.debug())<< Json::Compact(app_.getValidations().getJsonTrie()); } return netLgr; @@ -454,7 +446,8 @@ RCLConsensus::Adaptor::doAccept( app_.journal("LedgerConsensus").warn(), "Not validating"); - if (validating_ && !consensusFail) + if (validating_ && !consensusFail && + app_.getValidations().canValidateSeq(sharedLCL.seq())) { validate(sharedLCL, proposing); JLOG(j_.info()) << "CNF Val " << newLCLHash; @@ -841,7 +834,10 @@ RCLConsensus::Adaptor::validate(RCLCxLedger const& ledger, bool proposing) // Build validation auto v = std::make_shared( - ledger.id(), validationTime, valPublic_, proposing); + ledger.id(), + validationTime, + valPublic_, + proposing /* full if proposed */); v->setFieldU32(sfLedgerSequence, ledger.seq()); // Add our load fee to the validation diff --git a/src/ripple/app/consensus/RCLConsensus.h b/src/ripple/app/consensus/RCLConsensus.h index c200589b070..8bded9a15fc 100644 --- a/src/ripple/app/consensus/RCLConsensus.h +++ b/src/ripple/app/consensus/RCLConsensus.h @@ -201,12 +201,13 @@ class RCLConsensus /** Number of proposers that have validated a ledger descended from requested ledger. - @param h The hash of the ledger of interest. + @param ledger The current working ledger + @param h The hash of the preferred working ledger @return The number of validating peers that have validated a ledger - succeeding the one provided. + descended from the preferred working ledger. */ std::size_t - proposersFinished(LedgerHash const& h) const; + proposersFinished(RCLCxLedger const & ledger, LedgerHash const& h) const; /** Propose the given position to my peers. diff --git a/src/ripple/app/consensus/RCLValidations.cpp b/src/ripple/app/consensus/RCLValidations.cpp index 14c352fc2f0..85bf7fd69f7 100644 --- a/src/ripple/app/consensus/RCLValidations.cpp +++ b/src/ripple/app/consensus/RCLValidations.cpp @@ -19,6 +19,8 @@ #include #include +#include +#include #include #include #include @@ -36,19 +38,119 @@ namespace ripple { -RCLValidationsPolicy::RCLValidationsPolicy(Application& app) : app_(app) +RCLValidatedLedger::RCLValidatedLedger(MakeGenesis) + : ledgerID_{0}, ledgerSeq_{0} +{ +} + +RCLValidatedLedger::RCLValidatedLedger( + std::shared_ptr const& ledger, + beast::Journal j) + : ledgerID_{ledger->info().hash}, ledgerSeq_{ledger->seq()}, j_{j} +{ + auto const hashIndex = ledger->read(keylet::skip()); + if (hashIndex) + { + assert(hashIndex->getFieldU32(sfLastLedgerSequence) == (seq() - 1)); + ancestors_ = hashIndex->getFieldV256(sfHashes).value(); + } + else + JLOG(j_.warn()) << "Ledger " << ledgerSeq_ << ":" << ledgerID_ + << " missing recent ancestor hashes"; +} + +auto +RCLValidatedLedger::minSeq() const -> Seq +{ + return seq() - std::min(seq(), static_cast(ancestors_.size())); +} + +auto +RCLValidatedLedger::seq() const -> Seq +{ + return ledgerSeq_; +} +auto +RCLValidatedLedger::id() const -> ID +{ + return ledgerID_; +} + +auto RCLValidatedLedger::operator[](Seq const& s) const -> ID +{ + if (s >= minSeq() && s <= seq()) + { + if (s == seq()) + return ledgerID_; + Seq const diff = seq() - s; + return ancestors_[ancestors_.size() - diff]; + } + + JLOG(j_.warn()) << "Unable to determine hash of ancestor seq=" << s + << " from ledger hash=" << ledgerID_ + << " seq=" << ledgerSeq_; + // Default ID that is less than all others + return ID{0}; +} + +// Return the sequence number of the earliest possible mismatching ancestor +RCLValidatedLedger::Seq +mismatch(RCLValidatedLedger const& a, RCLValidatedLedger const& b) +{ + using Seq = RCLValidatedLedger::Seq; + + // Find overlapping interval for known sequence for the ledgers + Seq const lower = std::max(a.minSeq(), b.minSeq()); + Seq const upper = std::min(a.seq(), b.seq()); + + Seq curr = upper; + while (curr != Seq{0} && a[curr] != b[curr] && curr >= lower) + --curr; + + // If the searchable interval mismatches entirely, then we have to + // assume the ledgers mismatch starting post genesis ledger + return (curr < lower) ? Seq{1} : (curr + Seq{1}); +} + +RCLValidationsAdaptor::RCLValidationsAdaptor(Application& app, beast::Journal j) + : app_(app), j_(j) { staleValidations_.reserve(512); } NetClock::time_point -RCLValidationsPolicy::now() const +RCLValidationsAdaptor::now() const { return app_.timeKeeper().closeTime(); } +boost::optional +RCLValidationsAdaptor::acquire(LedgerHash const & hash) +{ + auto ledger = app_.getLedgerMaster().getLedgerByHash(hash); + if (!ledger) + { + JLOG(j_.debug()) + << "Need validated ledger for preferred ledger analysis " << hash; + + Application * pApp = &app_; + + app_.getJobQueue().addJob( + jtADVANCE, "getConsensusLedger", [pApp, hash](Job&) { + pApp ->getInboundLedgers().acquire( + hash, 0, InboundLedger::Reason::CONSENSUS); + }); + return boost::none; + } + + assert(!ledger->open() && ledger->isImmutable()); + assert(ledger->info().hash == hash); + + return RCLValidatedLedger(std::move(ledger), j_); +} + void -RCLValidationsPolicy::onStale(RCLValidation&& v) +RCLValidationsAdaptor::onStale(RCLValidation&& v) { // Store the newly stale validation; do not do significant work in this // function since this is a callback from Validations, which may be @@ -60,7 +162,7 @@ RCLValidationsPolicy::onStale(RCLValidation&& v) return; // addJob() may return false (Job not added) at shutdown. - staleWriting_ = app_.getJobQueue().addJob( + staleWriting_ = app_.getJobQueue().addJob( jtWRITE, "Validations::doStaleWrite", [this](Job&) { auto event = app_.getJobQueue().makeLoadEvent(jtDISK, "ValidationWrite"); @@ -70,7 +172,7 @@ RCLValidationsPolicy::onStale(RCLValidation&& v) } void -RCLValidationsPolicy::flush(hash_map&& remaining) +RCLValidationsAdaptor::flush(hash_map&& remaining) { bool anyNew = false; { @@ -106,7 +208,7 @@ RCLValidationsPolicy::flush(hash_map&& remaining) // NOTE: doStaleWrite() must be called with staleLock_ *locked*. The passed // ScopedLockType& acts as a reminder to future maintainers. void -RCLValidationsPolicy::doStaleWrite(ScopedLockType&) +RCLValidationsAdaptor::doStaleWrite(ScopedLockType&) { static const std::string insVal( "INSERT INTO Validations " @@ -131,10 +233,13 @@ RCLValidationsPolicy::doStaleWrite(ScopedLockType&) Serializer s(1024); soci::transaction tr(*db); - for (auto const& rclValidation : currentStale) + for (RCLValidation const& wValidation : currentStale) { + // Only save full validations until we update the schema + if(!wValidation.full()) + continue; s.erase(); - STValidation::pointer const& val = rclValidation.unwrap(); + STValidation::pointer const& val = wValidation.unwrap(); val->add(s); auto const ledgerHash = to_string(val->getLedgerHash()); @@ -174,97 +279,75 @@ handleNewValidation(Application& app, STValidation::ref val, std::string const& source) { - PublicKey const& signer = val->getSignerPublic(); + PublicKey const& signingKey = val->getSignerPublic(); uint256 const& hash = val->getLedgerHash(); // Ensure validation is marked as trusted if signer currently trusted - boost::optional pubKey = app.validators().getTrustedKey(signer); - if (!val->isTrusted() && pubKey) + boost::optional masterKey = + app.validators().getTrustedKey(signingKey); + if (!val->isTrusted() && masterKey) val->setTrusted(); - RCLValidations& validations = app.getValidations(); - - beast::Journal j = validations.journal(); - - // Do not process partial validations. - if (!val->isFull()) - { - const bool current = isCurrent( - validations.parms(), - app.timeKeeper().closeTime(), - val->getSignTime(), - val->getSeenTime()); - - JLOG(j.debug()) << "Val (partial) for " << hash << " from " - << toBase58(TokenType::TOKEN_NODE_PUBLIC, signer) - << " ignored " - << (val->isTrusted() ? "trusted/" : "UNtrusted/") - << (current ? "current" : "stale"); - - // Only forward if current and trusted - return current && val->isTrusted(); - } - - if (!val->isTrusted()) - { - JLOG(j.trace()) << "Node " - << toBase58(TokenType::TOKEN_NODE_PUBLIC, signer) - << " not in UNL st=" - << val->getSignTime().time_since_epoch().count() - << ", hash=" << hash - << ", shash=" << val->getSigningHash() - << " src=" << source; - } // If not currently trusted, see if signer is currently listed - if (!pubKey) - pubKey = app.validators().getListedKey(signer); + if (!masterKey) + masterKey = app.validators().getListedKey(signingKey); bool shouldRelay = false; - - // only add trusted or listed - if (pubKey) + RCLValidations& validations = app.getValidations(); + beast::Journal j = validations.adaptor().journal(); + + auto dmp = [&](beast::Journal::Stream s, std::string const& msg) { + s << "Val for " << hash + << (val->isTrusted() ? " trusted/" : " UNtrusted/") + << (val->isFull() ? "full" : "partial") << " from " + << (masterKey ? toBase58(TokenType::TOKEN_NODE_PUBLIC, *masterKey) + : "unknown") + << " signing key " + << toBase58(TokenType::TOKEN_NODE_PUBLIC, signingKey) << " " << msg + << " src=" << source; + }; + + if(!val->isFieldPresent(sfLedgerSequence)) { - using AddOutcome = RCLValidations::AddOutcome; - - AddOutcome const res = validations.add(*pubKey, val); + if(j.error()) + dmp(j.error(), "missing ledger sequence field"); + return false; + } - // This is a duplicate validation - if (res == AddOutcome::repeat) - return false; + // masterKey is seated only if validator is trusted or listed + if (masterKey) + { + ValStatus const outcome = validations.add(*masterKey, val); + if(j.debug()) + dmp(j.debug(), to_string(outcome)); - // This validation replaced a prior one with the same sequence number - if (res == AddOutcome::sameSeq) + if(outcome == ValStatus::badSeq && j.warn()) + { + auto const seq = val->getFieldU32(sfLedgerSequence); + dmp(j.warn(), + "already validated sequence at or past " + to_string(seq)); + } + else if(outcome == ValStatus::repeatID && j.warn()) { auto const seq = val->getFieldU32(sfLedgerSequence); - JLOG(j.warn()) << "Trusted node " - << toBase58(TokenType::TOKEN_NODE_PUBLIC, *pubKey) - << " published multiple validations for ledger " - << seq; + dmp(j.warn(), + "already validated ledger with same id but different seq " + "than" + to_string(seq)); } - JLOG(j.debug()) << "Val for " << hash << " from " - << toBase58(TokenType::TOKEN_NODE_PUBLIC, signer) - << " added " - << (val->isTrusted() ? "trusted/" : "UNtrusted/") - << ((res == AddOutcome::current) ? "current" : "stale"); - - // Trusted current validations should be checked and relayed. - // Trusted validations with sameSeq replaced an older validation - // with that sequence number, so should still be checked and relayed. - if (val->isTrusted() && - (res == AddOutcome::current || res == AddOutcome::sameSeq)) + if (val->isTrusted() && outcome == ValStatus::current) { app.getLedgerMaster().checkAccept( hash, val->getFieldU32(sfLedgerSequence)); - shouldRelay = true; } + } else { JLOG(j.debug()) << "Val for " << hash << " from " - << toBase58(TokenType::TOKEN_NODE_PUBLIC, signer) - << " not added UNtrusted/"; + << toBase58(TokenType::TOKEN_NODE_PUBLIC, signingKey) + << " not added UNlisted"; } // This currently never forwards untrusted validations, though we may @@ -277,4 +360,6 @@ handleNewValidation(Application& app, // ability/bandwidth to. None of that was implemented. return shouldRelay; } + + } // namespace ripple diff --git a/src/ripple/app/consensus/RCLValidations.h b/src/ripple/app/consensus/RCLValidations.h index cb8dbff0578..1bd3978eded 100644 --- a/src/ripple/app/consensus/RCLValidations.h +++ b/src/ripple/app/consensus/RCLValidations.h @@ -20,9 +20,11 @@ #ifndef RIPPLE_APP_CONSENSUSS_VALIDATIONS_H_INCLUDED #define RIPPLE_APP_CONSENSUSS_VALIDATIONS_H_INCLUDED +#include #include #include #include +#include #include #include @@ -39,6 +41,8 @@ class RCLValidation { STValidation::pointer val_; public: + using NodeKey = ripple::PublicKey; + using NodeID = ripple::NodeID; /** Constructor @@ -59,9 +63,7 @@ class RCLValidation std::uint32_t seq() const { - if(auto res = (*val_)[~sfLedgerSequence]) - return *res; - return 0; + return val_->getFieldU32(sfLedgerSequence); } /// Validation's signing time @@ -99,6 +101,13 @@ class RCLValidation return val_->isTrusted(); } + /// Whether the validatioon is full (not-partial) + bool + full() const + { + return val_->isFull(); + } + /// Get the load fee of the validation if it exists boost::optional loadFee() const @@ -115,35 +124,77 @@ class RCLValidation }; -/** Implements the StalePolicy policy class for adapting Validations in the RCL +/** Wraps a ledger instance for use in generic Validations LedgerTrie. - Manages storing and writing stale RCLValidations to the sqlite DB. + The LedgerTrie models a ledger's history as a map from Seq -> ID. Any + two ledgers that have the same ID for a given Seq have the same ID for + all earlier sequences (e.g. shared ancestry). In practice, a ledger only + conveniently has the prior 256 ancestor hashes available. For + RCLValidatedLedger, we treat any ledgers separated by more than 256 Seq as + distinct. */ -class RCLValidationsPolicy +class RCLValidatedLedger { - using LockType = std::mutex; - using ScopedLockType = std::lock_guard; - using ScopedUnlockType = GenericScopedUnlock; +public: + using ID = LedgerHash; + using Seq = LedgerIndex; + struct MakeGenesis + { + }; - Application& app_; + RCLValidatedLedger(MakeGenesis); - // Lock for managing staleValidations_ and writing_ - std::mutex staleLock_; - std::vector staleValidations_; - bool staleWriting_ = false; + RCLValidatedLedger( + std::shared_ptr const& ledger, + beast::Journal j); - // Write the stale validations to sqlite DB, the scoped lock argument - // is used to remind callers that the staleLock_ must be *locked* prior - // to making the call - void - doStaleWrite(ScopedLockType&); + /// The sequence (index) of the ledger + Seq + seq() const; + + /// The ID (hash) of the ledger + ID + id() const; + + /** Lookup the ID of the ancestor ledger + + @param s The sequence (index) of the ancestor + @return The ID of this ledger's ancestor with that sequence number or + ID{0} if one was not determined + */ + ID operator[](Seq const& s) const; + + /// Find the sequence number of the earliest mismatching ancestor + friend Seq + mismatch(RCLValidatedLedger const& a, RCLValidatedLedger const& b); + + Seq + minSeq() const; + +private: + ID ledgerID_; + Seq ledgerSeq_; + std::vector ancestors_; + beast::Journal j_; +}; + +/** Generic validations adaptor class for RCL + Manages storing and writing stale RCLValidations to the sqlite DB and + acquiring validated ledgers from the network. +*/ +class RCLValidationsAdaptor +{ public: + // Type definitions for generic Validation + using Mutex = std::mutex; + using Validation = RCLValidation; + using Ledger = RCLValidatedLedger; - RCLValidationsPolicy(Application & app); + RCLValidationsAdaptor(Application& app, beast::Journal j); /** Current time used to determine if validations are stale. - */ + */ NetClock::time_point now() const; @@ -163,20 +214,45 @@ class RCLValidationsPolicy @param remaining The remaining validations to flush */ void - flush(hash_map && remaining); -}; + flush(hash_map&& remaining); + + /** Attempt to acquire the ledger with given id from the network */ + boost::optional + acquire(LedgerHash const & id); + + beast::Journal + journal() const + { + return j_; + } + +private: + using ScopedLockType = std::lock_guard; + using ScopedUnlockType = GenericScopedUnlock; + + Application& app_; + beast::Journal j_; + + // Lock for managing staleValidations_ and writing_ + std::mutex staleLock_; + std::vector staleValidations_; + bool staleWriting_ = false; + // Write the stale validations to sqlite DB, the scoped lock argument + // is used to remind callers that the staleLock_ must be *locked* prior + // to making the call + void + doStaleWrite(ScopedLockType&); +}; /// Alias for RCL-specific instantiation of generic Validations -using RCLValidations = - Validations; +using RCLValidations = Validations; + /** Handle a new validation - 1. Set the trust status of a validation based on the validating node's - public key and this node's current UNL. - 2. Add the validation to the set of validations if current. - 3. If new and trusted, send the validation to the ledgerMaster. + Also sets the trust status of a validation based on the validating node's + public key and this node's current UNL. @param app Application object containing validations and ledgerMaster @param val The validation to add @@ -185,8 +261,10 @@ using RCLValidations = @return Whether the validation should be relayed */ bool -handleNewValidation(Application & app, STValidation::ref val, std::string const& source); - +handleNewValidation( + Application& app, + STValidation::ref val, + std::string const& source); } // namespace ripple diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 998edcbac93..1bf3408f9ef 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -497,8 +497,7 @@ class ApplicationImp stopwatch(), HashRouter::getDefaultHoldTime (), HashRouter::getDefaultRecoverLimit ())) - , mValidations (ValidationParms(),stopwatch(), logs_->journal("Validations"), - *this) + , mValidations (ValidationParms(),stopwatch(), *this, logs_->journal("Validations")) , m_loadManager (make_LoadManager (*this, *this, logs_->journal("LoadManager"))) @@ -916,7 +915,9 @@ class ApplicationImp // before we declare ourselves stopped. waitHandlerCounter_.join("Application", 1s, m_journal); + JLOG(m_journal.debug()) << "Flushing validations"; mValidations.flush (); + JLOG(m_journal.debug()) << "Validations flushed"; validatorSites_->stop (); diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index 032ae781b3b..6dcac7a3d5e 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -74,12 +74,10 @@ class SHAMapStore; using NodeCache = TaggedCache ; -template +template class Validations; -class RCLValidation; -class RCLValidationsPolicy; -using RCLValidations = - Validations; +class RCLValidationsAdaptor; +using RCLValidations = Validations; class Application : public beast::PropertyStream::Source { diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index fba0c3b5f68..583573880c5 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -1273,84 +1273,39 @@ bool NetworkOPsImp::checkLastClosedLedger ( JLOG(m_journal.trace()) << "OurClosed: " << closedLedger; JLOG(m_journal.trace()) << "PrevClosed: " << prevClosedLedger; - struct ValidationCount - { - std::uint32_t trustedValidations = 0; - std::uint32_t nodesUsing = 0; - }; - - hash_map ledgers; - { - hash_map current = - app_.getValidations().currentTrustedDistribution( - closedLedger, - prevClosedLedger, - m_ledgerMaster.getValidLedgerIndex()); - - for (auto& it: current) - ledgers[it.first].trustedValidations += it.second; - } + //------------------------------------------------------------------------- + // Determine preferred last closed ledger - auto& ourVC = ledgers[closedLedger]; + auto & validations = app_.getValidations(); + JLOG(m_journal.debug()) + << "ValidationTrie " << Json::Compact(validations.getJsonTrie()); + // Will rely on peer LCL if no trusted validations exist + hash_map peerCounts; + peerCounts[closedLedger] = 0; if (mMode >= omTRACKING) - { - ++ourVC.nodesUsing; - } + peerCounts[closedLedger]++; - for (auto& peer: peerList) + for (auto& peer : peerList) { - uint256 peerLedger = peer->getClosedLedgerHash (); + uint256 peerLedger = peer->getClosedLedgerHash(); - if (peerLedger.isNonZero ()) - ++ledgers[peerLedger].nodesUsing; + if (peerLedger.isNonZero()) + ++peerCounts[peerLedger]; } + for(auto const & it: peerCounts) + JLOG(m_journal.debug()) << "L: " << it.first << " n=" << it.second; - // 3) Is there a network ledger we'd like to switch to? If so, do we have - // it? - bool switchLedgers = false; - ValidationCount bestCounts = ledgers[closedLedger]; - - for (auto const& it: ledgers) - { - uint256 const & currLedger = it.first; - ValidationCount const & currCounts = it.second; - - JLOG(m_journal.debug()) << "L: " << currLedger - << " t=" << currCounts.trustedValidations - << ", n=" << currCounts.nodesUsing; - - bool const preferCurr = [&]() - { - // Prefer ledger with more trustedValidations - if (currCounts.trustedValidations > bestCounts.trustedValidations) - return true; - if (currCounts.trustedValidations < bestCounts.trustedValidations) - return false; - // If neither are trusted, prefer more nodesUsing - if (currCounts.trustedValidations == 0) - { - if (currCounts.nodesUsing > bestCounts.nodesUsing) - return true; - if (currCounts.nodesUsing < bestCounts.nodesUsing) - return false; - } - // If tied trustedValidations (non-zero) or tied nodesUsing, - // prefer higher ledger hash - return currLedger > closedLedger; - - }(); - - // Switch to current ledger if it is preferred over best so far - if (preferCurr) - { - bestCounts = currCounts; - closedLedger = currLedger; - switchLedgers = true; - } - } + uint256 preferredLCL = validations.getPreferredLCL( + RCLValidatedLedger{ourClosed, validations.adaptor().journal()}, + m_ledgerMaster.getValidLedgerIndex(), + peerCounts); + bool switchLedgers = preferredLCL != closedLedger; + if(switchLedgers) + closedLedger = preferredLCL; + //------------------------------------------------------------------------- if (switchLedgers && (closedLedger == prevClosedLedger)) { // don't switch to our own previous ledger @@ -1364,15 +1319,15 @@ bool NetworkOPsImp::checkLastClosedLedger ( if (!switchLedgers) return false; - auto consensus = m_ledgerMaster.getLedgerByHash (closedLedger); + auto consensus = m_ledgerMaster.getLedgerByHash(closedLedger); if (!consensus) - consensus = app_.getInboundLedgers().acquire ( + consensus = app_.getInboundLedgers().acquire( closedLedger, 0, InboundLedger::Reason::CONSENSUS); if (consensus && - ! m_ledgerMaster.isCompatible (*consensus, m_journal.debug(), - "Not switching")) + !m_ledgerMaster.isCompatible( + *consensus, m_journal.debug(), "Not switching")) { // Don't switch to a ledger not on the validated chain networkClosed = ourClosed->info().hash; @@ -1380,18 +1335,18 @@ bool NetworkOPsImp::checkLastClosedLedger ( } JLOG(m_journal.warn()) << "We are not running on the consensus ledger"; - JLOG(m_journal.info()) << "Our LCL: " << getJson (*ourClosed); + JLOG(m_journal.info()) << "Our LCL: " << getJson(*ourClosed); JLOG(m_journal.info()) << "Net LCL " << closedLedger; if ((mMode == omTRACKING) || (mMode == omFULL)) - setMode (omCONNECTED); + setMode(omCONNECTED); if (consensus) { - // FIXME: If this rewinds the ledger sequence, or has the same sequence, we - // should update the status on any stored transactions in the invalidated - // ledgers. - switchLastClosedLedger (consensus); + // FIXME: If this rewinds the ledger sequence, or has the same + // sequence, we should update the status on any stored transactions + // in the invalidated ledgers. + switchLastClosedLedger(consensus); } return true; diff --git a/src/ripple/consensus/Consensus.h b/src/ripple/consensus/Consensus.h index 891dd18b676..820fd9c3ce3 100644 --- a/src/ripple/consensus/Consensus.h +++ b/src/ripple/consensus/Consensus.h @@ -225,8 +225,10 @@ checkConsensus( std::size_t proposersValidated(Ledger::ID const & prevLedger) const; // Number of proposers that have validated a ledger descended from the - // given ledger - std::size_t proposersFinished(Ledger::ID const & prevLedger) const; + // given ledger; if prevLedger.id() != prevLedgerID, use prevLedgerID + // for the determination + std::size_t proposersFinished(Ledger const & prevLedger, + Ledger::ID const & prevLedger) const; // Return the ID of the last closed (and validated) ledger that the // application thinks consensus should use as the prior ledger. @@ -1410,7 +1412,8 @@ Consensus::haveConsensus() ++disagree; } } - auto currentFinished = adaptor_.proposersFinished(prevLedgerID_); + auto currentFinished = + adaptor_.proposersFinished(previousLedger_, prevLedgerID_); JLOG(j_.debug()) << "Checking for TX consensus: agree=" << agree << ", disagree=" << disagree; diff --git a/src/ripple/consensus/LedgerTrie.h b/src/ripple/consensus/LedgerTrie.h new file mode 100644 index 00000000000..a80c0471d3e --- /dev/null +++ b/src/ripple/consensus/LedgerTrie.h @@ -0,0 +1,815 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_CONSENSUS_LEDGERS_TRIE_H_INCLUDED +#define RIPPLE_APP_CONSENSUS_LEDGERS_TRIE_H_INCLUDED + +#include +#include +#include +#include +#include + +namespace ripple { + +/** The tip of a span of ledger ancestry +*/ +template +class SpanTip +{ +public: + using Seq = typename Ledger::Seq; + using ID = typename Ledger::ID; + + SpanTip(Seq s, ID i, Ledger const lgr) + : seq{s}, id{i}, ledger{std::move(lgr)} + { + } + + // The sequence number of the tip ledger + Seq seq; + // The ID of the tip ledger + ID id; + + /** Lookup the ID of an ancestor of the tip ledger + + @param s The sequence number of the ancestor + @return The ID of the ancestor with that sequence number + + @note s must be less than or equal to the sequence number of the + tip ledger + */ + ID + ancestor(Seq const& s) const + { + assert(s <= seq); + return ledger[s]; + } + +private: + Ledger const ledger; +}; + +namespace ledger_trie_detail { + +// Represents a span of ancestry of a ledger +template +class Span +{ + using Seq = typename Ledger::Seq; + using ID = typename Ledger::ID; + + // The span is the half-open interval [start,end) of ledger_ + Seq start_{0}; + Seq end_{1}; + Ledger ledger_; + +public: + Span() : ledger_{typename Ledger::MakeGenesis{}} + { + // Require default ledger to be genesis seq + assert(ledger_.seq() == start_); + } + + Span(Ledger ledger) + : start_{0}, end_{ledger.seq() + Seq{1}}, ledger_{std::move(ledger)} + { + } + + Span(Span const& s) = default; + Span(Span&& s) = default; + Span& + operator=(Span const&) = default; + Span& + operator=(Span&&) = default; + + Seq + start() const + { + return start_; + } + + Seq + end() const + { + return end_; + } + + // Return the Span from [spot,end_) or none if no such valid span + boost::optional + from(Seq spot) const + { + return sub(spot, end_); + } + + // Return the Span from [start_,spot) or none if no such valid span + boost::optional + before(Seq spot) const + { + return sub(start_, spot); + } + + // Return the ID of the ledger that starts this span + ID + startID() const + { + return ledger_[start_]; + } + + // Return the ledger sequence number of the first possible difference + // between this span and a given ledger. + Seq + diff(Ledger const& o) const + { + return clamp(mismatch(ledger_, o)); + } + + // The tip of this span + SpanTip + tip() const + { + Seq tipSeq{end_ - Seq{1}}; + return SpanTip{tipSeq, ledger_[tipSeq], ledger_}; + } + +private: + Span(Seq start, Seq end, Ledger const& l) + : start_{start}, end_{end}, ledger_{l} + { + // Spans cannot be empty + assert(start < end); + } + + Seq + clamp(Seq val) const + { + return std::min(std::max(start_, val), end_); + }; + + // Return a span of this over the half-open interval [from,to) + boost::optional + sub(Seq from, Seq to) const + { + Seq newFrom = clamp(from); + Seq newTo = clamp(to); + if (newFrom < newTo) + return Span(newFrom, newTo, ledger_); + return boost::none; + } + + friend std::ostream& + operator<<(std::ostream& o, Span const& s) + { + return o << s.tip().id << "[" << s.start_ << "," << s.end_ << ")"; + } + + friend Span + merge(Span const& a, Span const& b) + { + // Return combined span, using ledger_ from higher sequence span + if (a.end_ < b.end_) + return Span(std::min(a.start_, b.start_), b.end_, b.ledger_); + + return Span(std::min(a.start_, b.start_), a.end_, a.ledger_); + } +}; + +// A node in the trie +template +struct Node +{ + Node() = default; + + explicit Node(Ledger const& l) : span{l}, tipSupport{1}, branchSupport{1} + { + } + + explicit Node(Span s) : span{std::move(s)} + { + } + + Span span; + std::uint32_t tipSupport = 0; + std::uint32_t branchSupport = 0; + + std::vector> children; + Node* parent = nullptr; + + /** Remove the given node from this Node's children + + @param child The address of the child node to remove + @note The child must be a member of the vector. The passed pointer + will be dangling as a result of this call + */ + void + erase(Node const* child) + { + auto it = std::find_if( + children.begin(), + children.end(), + [child](std::unique_ptr const& curr) { + return curr.get() == child; + }); + assert(it != children.end()); + std::swap(*it, children.back()); + children.pop_back(); + } + + friend std::ostream& + operator<<(std::ostream& o, Node const& s) + { + return o << s.span << "(T:" << s.tipSupport << ",B:" << s.branchSupport + << ")"; + } + + Json::Value + getJson() const + { + Json::Value res; + res["id"] = to_string(span.tip().id); + res["seq"] = static_cast(span.tip().seq); + res["tipSupport"] = tipSupport; + res["branchSupport"] = branchSupport; + if (!children.empty()) + { + Json::Value& cs = (res["children"] = Json::arrayValue); + for (auto const& child : children) + { + cs.append(child->getJson()); + } + } + return res; + } +}; +} // namespace ledger_trie_detail + +/** Ancestry trie of ledgers + + A compressed trie tree that maintains validation support of recent ledgers + based on their ancestry. + + The compressed trie structure comes from recognizing that ledger history + can be viewed as a string over the alphabet of ledger ids. That is, + a given ledger with sequence number `seq` defines a length `seq` string, + with i-th entry equal to the id of the ancestor ledger with sequence + number i. "Sequence" strings with a common prefix share those ancestor + ledgers in common. Tracking this ancestry information and relations across + all validated ledgers is done conveniently in a compressed trie. A node in + the trie is an ancestor of all its children. If a parent node has sequence + number `seq`, each child node has a different ledger starting at `seq+1`. + The compression comes from the invariant that any non-root node with 0 tip + support has either no children or multiple children. In other words, a + non-root 0-tip-support node can be combined with its single child. + + Each node has a tipSupport, which is the number of current validations for + that particular ledger. The node's branch support is the sum of the tip + support and the branch support of that node's children: + + @code + node->branchSupport = node->tipSupport; + for (child : node->children) + node->branchSupport += child->branchSupport; + @endcode + + The templated Ledger type represents a ledger which has a unique history. + It should be lightweight and cheap to copy. + + @code + // Identifier types that should be equality-comparable and copyable + struct ID; + struct Seq; + + struct Ledger + { + struct MakeGenesis{}; + + // The genesis ledger represents a ledger that prefixes all other + // ledgers + Ledger(MakeGenesis{}); + + Ledger(Ledger const&); + Ledger& operator=(Ledger const&); + + // Return the sequence number of this ledger + Seq seq() const; + + // Return the ID of this ledger's ancestor with given sequence number + // or ID{0} if unknown + ID + operator[](Seq s); + + }; + + // Return the sequence number of the first possible mismatching ancestor + // between two ledgers + Seq + mismatch(ledgerA, ledgerB); + @endcode + + The unique history invariant of ledgers requires any ledgers that agree + on the id of a given sequence number agree on ALL ancestors before that + ledger: + + @code + Ledger a,b; + // For all Seq s: + if(a[s] == b[s]); + for(Seq p = 0; p < s; ++p) + assert(a[p] == b[p]); + @endcode + + @tparam Ledger A type representing a ledger and its history +*/ +template +class LedgerTrie +{ + using Seq = typename Ledger::Seq; + using ID = typename Ledger::ID; + + using Node = ledger_trie_detail::Node; + using Span = ledger_trie_detail::Span; + + // The root of the trie. The root is allowed to break the no-single child + // invariant. + std::unique_ptr root; + + // Count of the tip support for each sequence number + std::map seqSupport; + + /** Find the node in the trie that represents the longest common ancestry + with the given ledger. + + @return Pair of the found node and the sequence number of the first + ledger difference. + */ + std::pair + find(Ledger const& ledger) const + { + Node* curr = root.get(); + + // Root is always defined and is in common with all ledgers + assert(curr); + Seq pos = curr->span.diff(ledger); + + bool done = false; + + // Continue searching for a better span as long as the current position + // matches the entire span + while (!done && pos == curr->span.end()) + { + done = true; + // Find the child with the longest ancestry match + for (std::unique_ptr const& child : curr->children) + { + auto const childPos = child->span.diff(ledger); + if (childPos > pos) + { + done = false; + pos = childPos; + curr = child.get(); + break; + } + } + } + return std::make_pair(curr, pos); + } + + void + dumpImpl(std::ostream& o, std::unique_ptr const& curr, int offset) + const + { + if (curr) + { + if (offset > 0) + o << std::setw(offset) << "|-"; + + std::stringstream ss; + ss << *curr; + o << ss.str() << std::endl; + for (std::unique_ptr const& child : curr->children) + dumpImpl(o, child, offset + 1 + ss.str().size() + 2); + } + } + +public: + LedgerTrie() : root{std::make_unique()} + { + } + + /** Insert and/or increment the support for the given ledger. + + @param ledger A ledger and its ancestry + @param count The count of support for this ledger + */ + void + insert(Ledger const& ledger, std::uint32_t count = 1) + { + Node* loc; + Seq diffSeq; + std::tie(loc, diffSeq) = find(ledger); + + // There is always a place to insert + assert(loc); + + // Node from which to start incrementing branchSupport + Node* incNode = loc; + + // loc->span has the longest common prefix with Span{ledger} of all + // existing nodes in the trie. The optional's below represent + // the possible common suffixes between loc->span and Span{ledger}. + // + // loc->span + // a b c | d e f + // prefix | oldSuffix + // + // Span{ledger} + // a b c | g h i + // prefix | newSuffix + + boost::optional prefix = loc->span.before(diffSeq); + boost::optional oldSuffix = loc->span.from(diffSeq); + boost::optional newSuffix = Span{ledger}.from(diffSeq); + + if (oldSuffix) + { + // Have + // abcdef -> .... + // Inserting + // abc + // Becomes + // abc -> def -> ... + + // Create oldSuffix node that takes over loc + auto newNode = std::make_unique(*oldSuffix); + newNode->tipSupport = loc->tipSupport; + newNode->branchSupport = loc->branchSupport; + newNode->children = std::move(loc->children); + assert(loc->children.empty()); + for(std::unique_ptr & child : newNode->children) + child->parent = newNode.get(); + + // Loc truncates to prefix and newNode is its child + assert(prefix); + loc->span = *prefix; + newNode->parent = loc; + loc->children.emplace_back(std::move(newNode)); + loc->tipSupport = 0; + } + if (newSuffix) + { + // Have + // abc -> ... + // Inserting + // abcdef-> ... + // Becomes + // abc -> ... + // \-> def + + auto newNode = std::make_unique(*newSuffix); + newNode->parent = loc; + // increment support starting from the new node + incNode = newNode.get(); + loc->children.push_back(std::move(newNode)); + } + + incNode->tipSupport += count; + while (incNode) + { + incNode->branchSupport += count; + incNode = incNode->parent; + } + + seqSupport[ledger.seq()] += count; + } + + /** Decrease support for a ledger, removing and compressing if possible. + + @param ledger The ledger history to remove + @param count The amount of tip support to remove + + @return Whether a matching node was decremented and possibly removed. + */ + bool + remove(Ledger const& ledger, std::uint32_t count = 1) + { + Node* loc; + Seq diffSeq; + std::tie(loc, diffSeq) = find(ledger); + + // Cannot erase root + if (loc && loc != root.get()) + { + // Must be exact match with tip support + if (diffSeq == loc->span.end() && diffSeq > ledger.seq() && + loc->tipSupport > 0) + { + count = std::min(count, loc->tipSupport); + loc->tipSupport -= count; + + auto const it = seqSupport.find(ledger.seq()); + assert(it != seqSupport.end() && it->second >= count); + it->second -= count; + if(it->second == 0) + seqSupport.erase(it->first); + + Node* decNode = loc; + while (decNode) + { + decNode->branchSupport -= count; + decNode = decNode->parent; + } + + while (loc->tipSupport == 0 && loc != root.get()) + { + Node* parent = loc->parent; + if (loc->children.empty()) + { + // this node can be erased + parent->erase(loc); + } + else if (loc->children.size() == 1) + { + // This node can be combined with its child + std::unique_ptr child = + std::move(loc->children.front()); + child->span = merge(loc->span, child->span); + child->parent = parent; + parent->children.emplace_back(std::move(child)); + parent->erase(loc); + } + else + break; + loc = parent; + } + return true; + } + } + return false; + } + + /** Return count of tip support for the specific ledger. + + @param ledger The ledger to lookup + @return The number of entries in the trie for this *exact* ledger + */ + std::uint32_t + tipSupport(Ledger const& ledger) const + { + Node const* loc; + Seq diffSeq; + std::tie(loc, diffSeq) = find(ledger); + + // Exact match + if (loc && diffSeq == loc->span.end() && diffSeq > ledger.seq()) + return loc->tipSupport; + return 0; + } + + /** Return the count of branch support for the specific ledger + + @param ledger The ledger to lookup + @return The number of entries in the trie for this ledger or a descendant + */ + std::uint32_t + branchSupport(Ledger const& ledger) const + { + Node const* loc; + Seq diffSeq; + std::tie(loc, diffSeq) = find(ledger); + + // Check that ledger is is an exact match or proper + // prefix of loc + if (loc && diffSeq > ledger.seq() && + ledger.seq() < loc->span.end()) + { + return loc->branchSupport; + } + return 0; + } + + /** Return the preferred ledger ID + + The preferred ledger is used to determine the working ledger + for consensus amongst competing alternatives. + + Recall that each validator is normally validating a chain of ledgers, + e.g. A->B->C->D. However, if due to network connectivity or other + issues, validators generate different chains + + @code + /->C + A->B + \->D->E + @endcode + + we need a way for validators to converge on the chain with the most + support. We call this the preferred ledger. Intuitively, the idea is to + be conservative and only switch to a different branch when you see + enough peer validations to *know* another branch won't have preferred + support. + + The preferred ledger is found by walking this tree of validated ledgers + starting from the common ancestor ledger. + + At each sequence number, we have + + - The prior sequence preferred ledger, e.g. B. + - The (tip) support of ledgers with this sequence number,e.g. the + number of validators whose last validation was for C or D. + - The (branch) total support of all descendants of the current + sequence number ledgers, e.g. the branch support of D is the + tip support of D plus the tip support of E; the branch support of + C is just the tip support of C. + - The number of validators that have yet to validate a ledger + with this sequence number (uncommitted support). Uncommitted + includes all validators whose last sequence number is smaller than + our last issued sequence number, since due to asynchrony, we may + not have heard from those nodes yet. + + The preferred ledger for this sequence number is then the ledger + with relative majority of support, where uncommitted support + can be given to ANY ledger at that sequence number + (including one not yet known). If no such preferred ledger exists, then + the prior sequence preferred ledger is the overall preferred ledger. + + In this example, for D to be preferred, the number of validators + supporting it or a descendant must exceed the number of validators + supporting C _plus_ the current uncommitted support. This is because if + all uncommitted validators end up validating C, that new support must + be less than that for D to be preferred. + + If a preferred ledger does exist, then we continue with the next + sequence using that ledger as the root. + + @param largestIssued The sequence number of the largest validation + issued by this node. + @return Pair with the sequence number and ID of the preferred ledger + */ + SpanTip + getPreferred(Seq const largestIssued) const + { + Node* curr = root.get(); + + bool done = false; + + std::uint32_t uncommitted = 0; + auto uncommittedIt = seqSupport.begin(); + + while (curr && !done) + { + // Within a single span, the preferred by branch strategy is simply + // to continue along the span as long as the branch support of + // the next ledger exceeds the uncommitted support for that ledger. + { + // Add any initial uncommitted support prior for ledgers + // earlier than nextSeq or earlier than largestIssued + Seq nextSeq = curr->span.start() + Seq{1}; + while (uncommittedIt != seqSupport.end() && + uncommittedIt->first < std::max(nextSeq, largestIssued)) + { + uncommitted += uncommittedIt->second; + uncommittedIt++; + } + + // Advance nextSeq along the span + while (nextSeq < curr->span.end() && + curr->branchSupport > uncommitted) + { + // Jump to the next seqSupport change + if (uncommittedIt != seqSupport.end() && + uncommittedIt->first < curr->span.end()) + { + nextSeq = uncommittedIt->first + Seq{1}; + uncommitted += uncommittedIt->second; + uncommittedIt++; + } + else // otherwise we jump to the end of the span + nextSeq = curr->span.end(); + } + // We did not consume the entire span, so we have found the + // preferred ledger + if (nextSeq < curr->span.end()) + return curr->span.before(nextSeq)->tip(); + } + + // We have reached the end of the current span, so we need to + // find the best child + Node* best = nullptr; + std::uint32_t margin = 0; + if (curr->children.size() == 1) + { + best = curr->children[0].get(); + margin = best->branchSupport; + } + else if (!curr->children.empty()) + { + // Sort placing children with largest branch support in the + // front, breaking ties with the span's starting ID + std::partial_sort( + curr->children.begin(), + curr->children.begin() + 2, + curr->children.end(), + [](std::unique_ptr const& a, + std::unique_ptr const& b) { + return std::make_tuple(a->branchSupport, a->span.startID()) > + std::make_tuple(b->branchSupport, b->span.startID()); + }); + + best = curr->children[0].get(); + margin = curr->children[0]->branchSupport - + curr->children[1]->branchSupport; + + // If best holds the tie-breaker, gets one larger margin + // since the second best needs additional branchSupport + // to overcome the tie + if (best->span.startID() > curr->children[1]->span.startID()) + margin++; + } + + // If the best child has margin exceeding the uncommitted support, + // continue from that child, otherwise we are done + if (best && ((margin > uncommitted) || (uncommitted == 0))) + curr = best; + else // current is the best + done = true; + } + return curr->span.tip(); + } + + /** Dump an ascii representation of the trie to the stream + */ + void + dump(std::ostream& o) const + { + dumpImpl(o, root, 0); + } + + /** Dump JSON representation of trie state + */ + Json::Value + getJson() const + { + return root->getJson(); + } + + /** Check the compressed trie and support invariants. + */ + bool + checkInvariants() const + { + std::map expectedSeqSupport; + + std::stack nodes; + nodes.push(root.get()); + while (!nodes.empty()) + { + Node const* curr = nodes.top(); + nodes.pop(); + if (!curr) + continue; + + // Node with 0 tip support must have multiple children + // unless it is the root node + if (curr != root.get() && curr->tipSupport == 0 && + curr->children.size() < 2) + return false; + + // branchSupport = tipSupport + sum(child->branchSupport) + std::size_t support = curr->tipSupport; + if (curr->tipSupport != 0) + expectedSeqSupport[curr->span.end() - Seq{1}] += + curr->tipSupport; + + for (auto const& child : curr->children) + { + if(child->parent != curr) + return false; + + support += child->branchSupport; + nodes.push(child.get()); + } + if (support != curr->branchSupport) + return false; + } + return expectedSeqSupport == seqSupport; + } +}; + +} // namespace ripple +#endif diff --git a/src/ripple/consensus/Validations.h b/src/ripple/consensus/Validations.h index ad80698e5ab..52a06185266 100644 --- a/src/ripple/consensus/Validations.h +++ b/src/ripple/consensus/Validations.h @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include #include #include @@ -73,7 +73,49 @@ struct ValidationParms std::chrono::seconds validationSET_EXPIRES = std::chrono::minutes{10}; }; +/** Enforce validation increasing sequence requirement. + Helper class for enforcing that a validation must be larger than all + unexpired validation sequence numbers previously issued by the validator + tracked by the instance of this class. +*/ +template +class SeqEnforcer +{ + using time_point = std::chrono::steady_clock::time_point; + Seq seq_{0}; + time_point when_; +public: + /** Try advancing the largest observed validation ledger sequence + + Try setting the largest validation sequence observed, but return false + if it violates the invariant that a validation must be larger than all + unexpired validation sequence numbers. + + @param now The current time + @param s The sequence number we want to validate + @param p Validation parameters + + @return Whether the validation satisfies the invariant + */ + bool + operator()(time_point now, Seq s, ValidationParms const & p) + { + if(now > (when_ + p.validationSET_EXPIRES)) + seq_ = Seq{0}; + if(s <= seq_) + return false; + seq_ = s; + when_ = now; + return true; + } + + Seq + largest() const + { + return seq_; + } +}; /** Whether a validation is still current Determines whether a validation can still be considered the current @@ -103,35 +145,36 @@ 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) +/** Status of newly received validation + */ +enum class ValStatus { + /// This was a new validation and was added + current, + /// Already had this validation for this ID but different seq + repeatID, + /// Not current or was older than current from this node + stale, + /// A validation violates the increasing seq requirement + badSeq +}; + +inline std::string +to_string(ValStatus m) { - LedgerID netLgr = current; - int netLgrCount = 0; - for (auto const& it : dist) + switch (m) { - // 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; - } + case ValStatus::current: + return "current"; + case ValStatus::repeatID: + return "repeatID"; + case ValStatus::stale: + return "stale"; + case ValStatus::badSeq: + return "badSeq"; + default: + return "unknown"; } - return netLgr; } /** Maintains current and recent ledger validations. @@ -144,28 +187,30 @@ getPreferredLedger( and implementations should take care to use `trusted` member functions or check the validation's trusted status. - This class uses a policy design to allow adapting the handling of stale - validations in various circumstances. Below is a set of stubs illustrating - the required type interface. + This class uses a generic interface to allow adapting Validations for + specific applications. The Adaptor template implements a set of helper + functions and type definitions. The code stubs below outline the + interface and type requirements. + - @warning The MutexType is used to manage concurrent access to private - members of Validations but does not manage any data in the - StalePolicy instance. + @warning The Adaptor::MutexType is used to manage concurrent access to + private members of Validations but does not manage any data in the + Adaptor instance itself. @code - // Identifier types that should be equality-comparable and copyable - struct LedgerID; - struct NodeID; - struct NodeKey; + // Conforms to the Ledger type requirements of LedgerTrie + struct Ledger; struct Validation { + using NodeKey = ...; + // Ledger ID associated with this validation - LedgerID ledgerID() const; + Ledger::ID ledgerID() const; // Sequence number of validation's ledger (0 means no sequence number) - std::uint32_t seq() const + Ledger::Seq seq() const // When the validation was signed NetClock::time_point signTime() const; @@ -176,21 +221,25 @@ getPreferredLedger( // Signing key of node that published the validation NodeKey key() const; - // Identifier of node that published the validation - NodeID nodeID() const; - // Whether the publishing node was trusted at the time the validation // arrived bool trusted() const; + // Whether this is a full or partial validation + bool full() const; + implementation_specific_t unwrap() -> return the implementation-specific type being wrapped // ... implementation specific }; - class StalePolicy + class Adaptor { + using Mutex = std::mutex; + using Validation = Validation; + using Ledger = Ledger; + // Handle a newly stale validation, this should do minimal work since // it is called by Validations while it may be iterating Validations // under lock @@ -202,74 +251,201 @@ getPreferredLedger( // Return the current network time (used to determine staleness) NetClock::time_point now() const; + // Attempt to acquire a specific ledger. + boost::optional acquire(Ledger::ID const & ledgerID); + // ... implementation specific }; @endcode - @tparam StalePolicy Determines how to determine and handle stale validations - @tparam Validation Conforming type representing a ledger validation - @tparam MutexType Mutex used to manage concurrent access - + @tparam Adaptor Provides type definitions and callbacks */ -template +template class Validations { - template - using decay_result_t = std::decay_t>; + using Mutex = typename Adaptor::Mutex; + using Validation = typename Adaptor::Validation; + using Ledger = typename Adaptor::Ledger; + using ID = typename Ledger::ID; + using Seq = typename Ledger::Seq; + using NodeKey = typename Validation::NodeKey; - using WrappedValidationType = - decay_result_t; - using LedgerID = - decay_result_t; - using NodeKey = decay_result_t; - using NodeID = decay_result_t; - using SeqType = decay_result_t; + using WrappedValidationType = std::decay_t< + std::result_of_t>; + using ScopedLock = std::lock_guard; - using ScopedLock = std::lock_guard; + // Manages concurrent access to members + mutable Mutex mutex_; - // Manages concurrent access to current_ and byLedger_ - MutexType mutex_; + // Validations from currently listed and trusted nodes (partial and full) + hash_map current_; - //! For the most recent validation, we also want to store the ID - //! of the ledger it replaces - struct ValidationAndPrevID - { - ValidationAndPrevID(Validation const& v) : val{v}, prevLedgerID{0} - { - } - - Validation val; - LedgerID prevLedgerID; - }; + // Used to enforce the largest validation invariant for the local node + SeqEnforcer localSeqEnforcer_; - //! The latest validation from each node - hash_map current_; + // Sequence of the largest validation received from each node + hash_map> seqEnforcers_; - //! Recent validations from nodes, indexed by ledger identifier + //! Validations from listed nodes, indexed by ledger id (partial and full) beast::aged_unordered_map< - LedgerID, + ID, hash_map, std::chrono::steady_clock, beast::uhash<>> byLedger_; - //! Parameters to determine validation staleness - ValidationParms const parms_; + // Represents the ancestry of validated ledgers + LedgerTrie trie_; + + // Last (validated) ledger successfully acquired. If in this map, it is + // accounted for in the trie. + hash_map lastLedger_; - beast::Journal j_; + // Set of ledgers being acquired from the network + hash_map, hash_set> acquiring_; - //! StalePolicy details providing now(), onStale() and flush() callbacks - //! Is NOT managed by the mutex_ above - StalePolicy stalePolicy_; + // Parameters to determine validation staleness + ValidationParms const parms_; + + // Adaptor instance + // Is NOT managed by the mutex_ above + Adaptor adaptor_; private: + // Remove support of a validated ledger + void + removeTrie(ScopedLock const&, NodeKey const& key, Validation const& val) + { + { + auto it = acquiring_.find(std::make_pair(val.seq(), val.ledgerID())); + if (it != acquiring_.end()) + { + it->second.erase(key); + if (it->second.empty()) + acquiring_.erase(it); + } + } + { + auto it = lastLedger_.find(key); + if (it != lastLedger_.end() && it->second.id() == val.ledgerID()) + { + trie_.remove(it->second); + lastLedger_.erase(key); + } + } + } + + // Check if any pending acquire ledger requests are complete + void + checkAcquired(ScopedLock const& lock) + { + for (auto it = acquiring_.begin(); it != acquiring_.end();) + { + if (boost::optional ledger = + adaptor_.acquire(it->first.second)) + { + for (NodeKey const& key : it->second) + updateTrie(lock, key, *ledger); + + it = acquiring_.erase(it); + } + else + ++it; + } + } + + // Update the trie to reflect a new validated ledger + void + updateTrie(ScopedLock const&, NodeKey const& key, Ledger ledger) + { + auto ins = lastLedger_.emplace(key, ledger); + if (!ins.second) + { + trie_.remove(ins.first->second); + ins.first->second = ledger; + } + trie_.insert(ledger); + } + + /** Process a new validation + + Process a new trusted validation from a validator. This will be + reflected only after the validated ledger is successfully acquired by + the local node. In the interim, the prior validated ledger from this + node remains. + + @param lock Existing lock of mutex_ + @param key The master public key identifying the validating node + @param val The trusted validation issued by the node + @param prior If not none, the last current validated ledger Seq,ID of key + */ + void + updateTrie( + ScopedLock const& lock, + NodeKey const& key, + Validation const& val, + boost::optional> prior) + { + assert(val.trusted()); + + // Clear any prior acquiring ledger for this node + if (prior) + { + auto it = acquiring_.find(*prior); + if (it != acquiring_.end()) + { + it->second.erase(key); + if (it->second.empty()) + acquiring_.erase(it); + } + } + + checkAcquired(lock); + + std::pair valPair{val.seq(),val.ledgerID()}; + auto it = acquiring_.find(valPair); + if(it != acquiring_.end()) + { + it->second.insert(key); + } + else + { + if (boost::optional ledger = adaptor_.acquire(val.ledgerID())) + updateTrie(lock, key, *ledger); + else + acquiring_[valPair].insert(key); + } + + } + + /** Use the trie for a calculation + + Accessing the trie through this helper ensures acquiring validations + are checked and any stale validations are flushed from the trie. + + @param lock Existing lock of mutex_ + @param f Invokable with signature (LedgerTrie &) + + @warning The invokable `f` is expected to be a simple transformation of + its arguments and will be called with mutex_ under lock. + + */ + template + auto + withTrie(ScopedLock const& lock, F&& f) + { + // Call current to flush any stale validations + current(lock, [](auto){}, [](auto, auto){}); + checkAcquired(lock); + return f(trie_); + } + /** Iterate current validations. - Iterate current validations, optionally removing any stale validations - if a time is specified. + Iterate current validations, flushing any which are stale. - @param t (Optional) Time used to determine staleness + @param lock Existing lock of mutex_ @param pre Invokable with signature (std::size_t) called prior to looping. @param f Invokable with signature (NodeKey const &, Validations const &) @@ -283,20 +459,19 @@ class Validations template void - current(boost::optional t, Pre&& pre, F&& f) + current(ScopedLock const& lock, Pre&& pre, F&& f) { - ScopedLock lock{mutex_}; + NetClock::time_point t = adaptor_.now(); pre(current_.size()); auto it = current_.begin(); while (it != current_.end()) { - // Check for staleness, if time specified - if (t && - !isCurrent( - parms_, *t, it->second.val.signTime(), it->second.val.seenTime())) + // Check for staleness + if (!isCurrent( + parms_, t, it->second.signTime(), it->second.seenTime())) { - // contains a stale record - stalePolicy_.onStale(std::move(it->second.val)); + removeTrie(lock, it->first, it->second); + adaptor_.onStale(std::move(it->second)); it = current_.erase(it); } else @@ -311,6 +486,7 @@ class Validations /** Iterate the set of validations associated with a given ledger id + @param lock Existing lock on mutex_ @param ledgerID The identifier of the ledger @param pre Invokable with signature(std::size_t) @param f Invokable with signature (NodeKey const &, Validation const &) @@ -322,9 +498,8 @@ class Validations */ template void - byLedger(LedgerID const& ledgerID, Pre&& pre, F&& f) + byLedger(ScopedLock const&, ID const& ledgerID, Pre&& pre, F&& f) { - ScopedLock lock{mutex_}; auto it = byLedger_.find(ledgerID); if (it != byLedger_.end()) { @@ -341,17 +516,23 @@ class Validations @param p ValidationParms to control staleness/expiration of validaitons @param c Clock to use for expiring validations stored by ledger - @param j Journal used for logging - @param ts Parameters for constructing StalePolicy instance + @param ts Parameters for constructing Adaptor instance */ template Validations( ValidationParms const& p, beast::abstract_clock& c, - beast::Journal j, Ts&&... ts) - : byLedger_(c), parms_(p), j_(j), stalePolicy_(std::forward(ts)...) + : byLedger_(c), parms_(p), adaptor_(std::forward(ts)...) + { + } + + /** Return the adaptor instance + */ + Adaptor const & + adaptor() const { + return adaptor_; } /** Return the validation timing parameters @@ -362,148 +543,75 @@ class Validations return parms_; } - /** Return the journal - */ - beast::Journal - journal() const + /** Return whether the local node can issue a validation for the given sequence + number + + @param s The sequence number of the ledger the node wants to validate + @return Whether the validation satisfies the invariant, updating the + largest sequence number seen accordingly + */ + bool + canValidateSeq(Seq const s) { - return j_; + ScopedLock lock{mutex_}; + return localSeqEnforcer_(byLedger_.clock().now(), s, parms_); } - /** Result of adding a new validation - */ - enum class AddOutcome { - /// This was a new validation and was added - current, - /// Already had this validation - repeat, - /// Not current or was older than current from this node - stale, - /// Had a validation with same sequence number - sameSeq, - }; - /** Add a new validation Attempt to add a new validation. - @param key The NodeKey to use for the validation + @param key The master key associated with this validation @param val The validation to store - @return The outcome of the attempt - - @note The provided key may differ from the validation's - key() member since we might be storing by master key and the - validation might be signed by a temporary or rotating key. + @return The outcome + @note The provided key may differ from the validation's key() + member if the validator is using ephemeral signing keys. */ - AddOutcome + ValStatus add(NodeKey const& key, Validation const& val) { - NetClock::time_point t = stalePolicy_.now(); - if (!isCurrent(parms_, t, val.signTime(), val.seenTime())) - return AddOutcome::stale; - - LedgerID const& id = val.ledgerID(); - - // This is only seated if a validation became stale - boost::optional maybeStaleValidation; - - AddOutcome result = AddOutcome::current; + if (!isCurrent(parms_, adaptor_.now(), val.signTime(), val.seenTime())) + return ValStatus::stale; { ScopedLock lock{mutex_}; - auto const ret = byLedger_[id].emplace(key, val); + // Check that validation sequence is greater than any non-expired + // validations sequence from that validator + auto const now = byLedger_.clock().now(); + SeqEnforcer& enforcer = seqEnforcers_[key]; + if (!enforcer(now, val.seq(), parms_)) + return ValStatus::badSeq; // This validation is a repeat if we already have - // one with the same id and signing key. + // one with the same id for this key + auto const ret = byLedger_[val.ledgerID()].emplace(key, val); if (!ret.second && ret.first->second.key() == val.key()) - return AddOutcome::repeat; + return ValStatus::repeatID; - // Attempt to insert auto const ins = current_.emplace(key, val); - if (!ins.second) { - // Had a previous validation from the node, consider updating - Validation& oldVal = ins.first->second.val; - LedgerID const previousLedgerID = ins.first->second.prevLedgerID; - - SeqType const oldSeq{oldVal.seq()}; - SeqType const newSeq{val.seq()}; - - // Sequence of 0 indicates a missing sequence number - if ((oldSeq != SeqType{0}) && (newSeq != SeqType{0}) && - oldSeq == newSeq) + // Replace existing only if this one is newer + Validation& oldVal = ins.first->second; + if (val.signTime() > oldVal.signTime()) { - result = AddOutcome::sameSeq; - - // If the validation key was revoked, update the - // existing validation in the byLedger_ set - if (val.key() != oldVal.key()) - { - auto const mapIt = byLedger_.find(oldVal.ledgerID()); - if (mapIt != byLedger_.end()) - { - auto& validationMap = mapIt->second; - // If a new validation with the same ID was - // reissued we simply replace. - if(oldVal.ledgerID() == val.ledgerID()) - { - auto replaceRes = validationMap.emplace(key, val); - // If it was already there, replace - if(!replaceRes.second) - replaceRes.first->second = val; - } - else - { - // If the new validation has a different ID, - // we remove the old. - validationMap.erase(key); - // Erase the set if it is now empty - if (validationMap.empty()) - byLedger_.erase(mapIt); - } - } - } - } - - if (val.signTime() > oldVal.signTime() || - val.key() != oldVal.key()) - { - // This is either a newer validation or a new signing key - LedgerID const prevID = [&]() { - // In the normal case, the prevID is the ID of the - // ledger we replace - if (oldVal.ledgerID() != val.ledgerID()) - return oldVal.ledgerID(); - // In the case the key was revoked and a new validation - // for the same ledger ID was sent, the previous ledger - // is still the one the now revoked validation had - return previousLedgerID; - }(); - - // Allow impl to take over oldVal - maybeStaleValidation.emplace(std::move(oldVal)); - // Replace old val in the map and set the previous ledger ID - ins.first->second.val = val; - ins.first->second.prevLedgerID = prevID; + std::pair old(oldVal.seq(),oldVal.ledgerID()); + adaptor_.onStale(std::move(oldVal)); + ins.first->second = val; + if (val.trusted()) + updateTrie(lock, key, val, old); } else - { - // We already have a newer validation from this source - result = AddOutcome::stale; - } + return ValStatus::stale; + } + else if (val.trusted()) + { + updateTrie(lock, key, val, boost::none); } } - - // Handle the newly stale validation outside the lock - if (maybeStaleValidation) - { - stalePolicy_.onStale(std::move(*maybeStaleValidation)); - } - - return result; + return ValStatus::current; } /** Expire old validation sets @@ -518,103 +626,173 @@ class Validations beast::expire(byLedger_, parms_.validationSET_EXPIRES); } - /** Distribution of current trusted validations + Json::Value + getJsonTrie() const + { + ScopedLock lock{mutex_}; + return trie_.getJson(); + } + + /** Return the sequence number and ID of the preferred working ledger - Calculates the distribution of current validations but allows - ledgers one away from the current ledger to count as the current. + A ledger is preferred if it has more support amongst trusted validators + and is *not* an ancestor of the current working ledger; otherwise it + remains the current working ledger. - @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 + @param curr The local node's current working ledger - @return Map representing the distribution of ledgerID by count + @return The sequence and id of the preferred working ledger, + or Seq{0},ID{0} if no trusted validations are available to + determine the preferred ledger. */ - hash_map - currentTrustedDistribution( - LedgerID const& currentLedger, - LedgerID const& priorLedger, - SeqType cutoffBefore) + std::pair + getPreferred(Ledger const& curr) { - bool const valCurrentLedger = currentLedger != LedgerID{0}; - bool const valPriorLedger = priorLedger != LedgerID{0}; + ScopedLock lock{mutex_}; + SpanTip preferred = + withTrie(lock, [this](LedgerTrie& trie) { + return trie.getPreferred(localSeqEnforcer_.largest()); + }); - hash_map ret; + // No trusted validations to determine branch + if (preferred.seq == Seq{0}) + { + // fall back to majority over acquiring ledgers + auto it = std::max_element( + acquiring_.begin(), + acquiring_.end(), + [](auto const& a, auto const& b) { + std::pair const& aKey = a.first; + typename hash_set::size_type const& aSize = + a.second.size(); + std::pair const& bKey = b.first; + typename hash_set::size_type const& bSize = + b.second.size(); + // order by number of trusted peers validating that ledger + // break ties with ledger ID + return std::tie(aSize, aKey.second) < + std::tie(bSize, bKey.second); + }); + if(it != acquiring_.end()) + return it->first; + return std::make_pair(preferred.seq, preferred.id); + } - current( - stalePolicy_.now(), - // The number of validations does not correspond to the number of - // distinct ledgerIDs so we do not call reserve on ret. - [](std::size_t) {}, - [this, - &cutoffBefore, - ¤tLedger, - &valCurrentLedger, - &valPriorLedger, - &priorLedger, - &ret](NodeKey const&, ValidationAndPrevID const& vp) { - Validation const& v = vp.val; - LedgerID const& prevLedgerID = vp.prevLedgerID; - if (!v.trusted()) - return; - - SeqType const seq = v.seq(); - if ((seq == SeqType{0}) || (seq >= cutoffBefore)) - { - // contains a live record - bool countPreferred = - valCurrentLedger && (v.ledgerID() == currentLedger); - - if (!countPreferred && // allow up to one ledger slip in - // either direction - ((valCurrentLedger && - (prevLedgerID == currentLedger)) || - (valPriorLedger && (v.ledgerID() == priorLedger)))) - { - countPreferred = true; - JLOG(this->j_.trace()) << "Counting for " << currentLedger - << " not " << v.ledgerID(); - } - - if (countPreferred) - ret[currentLedger]++; - else - ret[v.ledgerID()]++; - } + // If we are the parent of the preferred ledger, stick with our + // current ledger since we might be about to generate it + if (preferred.seq == curr.seq() + Seq{1} && + preferred.ancestor(curr.seq()) == curr.id()) + return std::make_pair(curr.seq(), curr.id()); + + // A ledger ahead of us is preferred regardless of whether it is + // a descendant of our working ledger or it is on a different chain + if (preferred.seq > curr.seq()) + return std::make_pair(preferred.seq, preferred.id); + + // Only switch to earlier or same sequence number + // if it is a different chain. + if (curr[preferred.seq] != preferred.id) + return std::make_pair(preferred.seq, preferred.id); + + // Stick with current ledger + return std::make_pair(curr.seq(), curr.id()); + } + + /** Get the ID of the preferred working ledger that exceeds a minimum valid + ledger sequence number + + @param curr Current working ledger + @param minValidSeq Minimum allowed sequence number + + @return ID Of the preferred ledger, or curr if the preferred ledger + is not valid + */ + ID + getPreferred(Ledger const& curr, Seq minValidSeq) + { + std::pair preferred = getPreferred(curr); + if(preferred.first >= minValidSeq && preferred.second != ID{0}) + return preferred.second; + return curr.id(); + + } + + + /** Determine the preferred last closed ledger for the next consensus round. + + Called before starting the next round of ledger consensus to determine the + preferred working ledger. Uses the dominant peerCount ledger if no + trusted validations are available. + + @param lcl Last closed ledger by this node + @param minSeq Minimum allowed sequence number of the trusted preferred ledger + @param peerCounts Map from ledger ids to count of peers with that as the + last closed ledger + @return The preferred last closed ledger ID + + @note The minSeq does not apply to the peerCounts, since this function + does not know their sequence number + */ + ID + getPreferredLCL( + Ledger const & lcl, + Seq minSeq, + hash_map const& peerCounts) + { + std::pair preferred = getPreferred(lcl); + + // Trusted validations exist + if (preferred.second != ID{0} && preferred.first > Seq{0}) + return (preferred.first >= minSeq) ? preferred.second : lcl.id(); + + // Otherwise, rely on peer ledgers + auto it = std::max_element( + peerCounts.begin(), peerCounts.end(), [](auto& a, auto& b) { + // Prefer larger counts, then larger ids on ties + // (max_element expects this to return true if a < b) + return std::tie(a.second, a.first) < + std::tie(b.second, b.first); }); - return ret; + if (it != peerCounts.end()) + return it->first; + return lcl.id(); } - /** Count the number of current trusted validators working on the next - ledger. + /** Count the number of current trusted validators working on a ledger + after the specified one. - Counts the number of current trusted validations that replaced the - provided ledger. Does not check or update staleness of the validations. + @param ledger The working ledger + @param ledgerID The preferred ledger + @return The number of current trusted validators working on a descendent + of the preferred ledger - @param ledgerID The identifier of the preceding ledger of interest - @return The number of current trusted validators with ledgerID as the - prior ledger. + @note If ledger.id() != ledgerID, only counts immediate child ledgers of + ledgerID */ std::size_t - getNodesAfter(LedgerID const& ledgerID) + getNodesAfter(Ledger const& ledger, ID const& ledgerID) { - std::size_t count = 0; + ScopedLock lock{mutex_}; - // Historically this did not not check for stale validations - // That may not be important, but this preserves the behavior - current( - boost::none, - [&](std::size_t) {}, // nothing to reserve - [&](NodeKey const&, ValidationAndPrevID const& v) { - if (v.val.trusted() && v.prevLedgerID == ledgerID) - ++count; + // Use trie if ledger is the right one + if (ledger.id() == ledgerID) + return withTrie(lock, [&ledger](LedgerTrie& trie) { + return trie.branchSupport(ledger) - trie.tipSupport(ledger); + }); + + // Count parent ledgers as fallback + return std::count_if( + lastLedger_.begin(), + lastLedger_.end(), + [&ledgerID](auto const& it) { + auto const& curr = it.second; + return curr.seq() > Seq{0} && + curr[curr.seq() - Seq{1}] == ledgerID; }); - return count; } - /** Get the currently trusted validations + /** Get the currently trusted full validations @return Vector of validations from currently trusted validators */ @@ -622,107 +800,115 @@ class Validations currentTrusted() { std::vector ret; - + ScopedLock lock{mutex_}; current( - stalePolicy_.now(), + lock, [&](std::size_t numValidations) { ret.reserve(numValidations); }, - [&](NodeKey const&, ValidationAndPrevID const& v) { - if (v.val.trusted()) - ret.push_back(v.val.unwrap()); + [&](NodeKey const&, Validation const& v) { + if (v.trusted() && v.full()) + ret.push_back(v.unwrap()); }); return ret; } /** Get the set of known public keys associated with current validations - @return The set of of knowns keys for current trusted and untrusted - validations + @return The set of known keys for current listed validators */ hash_set getCurrentPublicKeys() { hash_set ret; + ScopedLock lock{mutex_}; current( - stalePolicy_.now(), + lock, [&](std::size_t numValidations) { ret.reserve(numValidations); }, - [&](NodeKey const& k, ValidationAndPrevID const&) { ret.insert(k); }); + [&](NodeKey const& k, Validation const&) { ret.insert(k); }); return ret; } - /** Count the number of trusted validations for the given ledger + /** Count the number of trusted full validations for the given ledger @param ledgerID The identifier of ledger of interest @return The number of trusted validations */ std::size_t - numTrustedForLedger(LedgerID const& ledgerID) + numTrustedForLedger(ID const& ledgerID) { std::size_t count = 0; + ScopedLock lock{mutex_}; byLedger( + lock, ledgerID, - [&](std::size_t) {}, // nothing to reserve + [&](std::size_t) {}, // nothing to reserve [&](NodeKey const&, Validation const& v) { - if (v.trusted()) + if (v.trusted() && v.full()) ++count; }); return count; } - /** Get set of trusted validations associated with a given ledger + /** Get trusted full validations for a specific ledger @param ledgerID The identifier of ledger of interest @return Trusted validations associated with ledger */ std::vector - getTrustedForLedger(LedgerID const& ledgerID) + getTrustedForLedger(ID const& ledgerID) { std::vector res; + ScopedLock lock{mutex_}; byLedger( + lock, ledgerID, [&](std::size_t numValidations) { res.reserve(numValidations); }, [&](NodeKey const&, Validation const& v) { - if (v.trusted()) + if (v.trusted() && v.full()) res.emplace_back(v.unwrap()); }); return res; } - /** Return the sign times of all validations associated with a given ledger + /** Return the sign times of all trusted full validations @param ledgerID The identifier of ledger of interest @return Vector of times */ std::vector - getTrustedValidationTimes(LedgerID const& ledgerID) + getTrustedValidationTimes(ID const& ledgerID) { std::vector times; + ScopedLock lock{mutex_}; byLedger( + lock, ledgerID, [&](std::size_t numValidations) { times.reserve(numValidations); }, [&](NodeKey const&, Validation const& v) { - if (v.trusted()) + if (v.trusted() && v.full()) times.emplace_back(v.signTime()); }); return times; } - /** Returns fees reported by trusted validators in the given ledger + /** Returns fees reported by trusted full validators in the given ledger @param ledgerID The identifier of ledger of interest @param baseFee The fee to report if not present in the validation @return Vector of fees */ std::vector - fees(LedgerID const& ledgerID, std::uint32_t baseFee) + fees(ID const& ledgerID, std::uint32_t baseFee) { std::vector res; + ScopedLock lock{mutex_}; byLedger( + lock, ledgerID, [&](std::size_t numValidations) { res.reserve(numValidations); }, [&](NodeKey const&, Validation const& v) { - if (v.trusted()) + if (v.trusted() && v.full()) { boost::optional loadFee = v.loadFee(); if (loadFee) @@ -739,22 +925,19 @@ class Validations void flush() { - JLOG(j_.info()) << "Flushing validations"; - hash_map flushed; { ScopedLock lock{mutex_}; for (auto it : current_) { - flushed.emplace(it.first, std::move(it.second.val)); + flushed.emplace(it.first, std::move(it.second)); } current_.clear(); } - stalePolicy_.flush(std::move(flushed)); - - JLOG(j_.debug()) << "Validations flushed"; + adaptor_.flush(std::move(flushed)); } }; + } // namespace ripple #endif diff --git a/src/test/app/RCLValidations_test.cpp b/src/test/app/RCLValidations_test.cpp new file mode 100644 index 00000000000..0feeb2b7a80 --- /dev/null +++ b/src/test/app/RCLValidations_test.cpp @@ -0,0 +1,204 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright 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 +#include +#include + +namespace ripple { +namespace test { + +class RCLValidations_test : public beast::unit_test::suite +{ + +public: + void + run() override + { + beast::Journal j; + + using Seq = RCLValidatedLedger::Seq; + using ID = RCLValidatedLedger::ID; + + // This tests RCLValidatedLedger properly implements the type + // requirements of a LedgerTrie ledger, with its added behavior that + // only the 256 prior ledger hashes are available to determine ancestry. + Seq const maxAncestors = 256; + + //---------------------------------------------------------------------- + // Generate two ledger histories that agree on the first maxAncestors + // ledgers, then diverge. + + std::vector> history; + + jtx::Env env(*this); + Config config; + auto prev = std::make_shared( + create_genesis, config, + std::vector{}, env.app().family()); + history.push_back(prev); + for (auto i = 0; i < (2*maxAncestors + 1); ++i) + { + auto next = std::make_shared( + *prev, + env.app().timeKeeper().closeTime()); + next->updateSkipList(); + history.push_back(next); + prev = next; + } + + // altHistory agrees with first half of regular history + Seq const diverge = history.size()/2; + std::vector> altHistory( + history.begin(), history.begin() + diverge); + // advance clock to get new ledgers + env.timeKeeper().set(env.timeKeeper().now() + 1200s); + prev = altHistory.back(); + bool forceHash = true; + while(altHistory.size() < history.size()) + { + auto next = std::make_shared( + *prev, + env.app().timeKeeper().closeTime()); + // Force a different hash on the first iteration + next->updateSkipList(); + if(forceHash) + { + next->setImmutable(config); + forceHash = false; + } + + altHistory.push_back(next); + prev = next; + } + + //---------------------------------------------------------------------- + + + // Empty ledger + { + RCLValidatedLedger a{RCLValidatedLedger::MakeGenesis{}}; + BEAST_EXPECT(a.seq() == Seq{0}); + BEAST_EXPECT(a[Seq{0}] == ID{0}); + BEAST_EXPECT(a.minSeq() == Seq{0}); + } + + // Full history ledgers + { + std::shared_ptr ledger = history.back(); + RCLValidatedLedger a{ledger, j}; + BEAST_EXPECT(a.seq() == ledger->info().seq); + BEAST_EXPECT( + a.minSeq() == a.seq() - maxAncestors); + // Ensure the ancestral 256 ledgers have proper ID + for(Seq s = a.seq(); s > 0; s--) + { + if(s >= a.minSeq()) + BEAST_EXPECT(a[s] == history[s-1]->info().hash); + else + BEAST_EXPECT(a[s] == ID{0}); + } + } + + // Mismatch tests + + // Empty with non-empty + { + RCLValidatedLedger a{RCLValidatedLedger::MakeGenesis{}}; + + for (auto ledger : {history.back(), + history[maxAncestors - 1]}) + { + RCLValidatedLedger b{ledger, j}; + BEAST_EXPECT(mismatch(a, b) == 1); + BEAST_EXPECT(mismatch(b, a) == 1); + } + } + // Same chains, different seqs + { + RCLValidatedLedger a{history.back(), j}; + for(Seq s = a.seq(); s > 0; s--) + { + RCLValidatedLedger b{history[s-1], j}; + if(s >= a.minSeq()) + { + BEAST_EXPECT(mismatch(a, b) == b.seq() + 1); + BEAST_EXPECT(mismatch(b, a) == b.seq() + 1); + } + else + { + BEAST_EXPECT(mismatch(a, b) == Seq{1}); + BEAST_EXPECT(mismatch(b, a) == Seq{1}); + } + } + + } + // Different chains, same seqs + { + // Alt history diverged at history.size()/2 + for(Seq s = 1; s < history.size(); ++s) + { + RCLValidatedLedger a{history[s-1], j}; + RCLValidatedLedger b{altHistory[s-1], j}; + + BEAST_EXPECT(a.seq() == b.seq()); + if(s <= diverge) + { + BEAST_EXPECT(a[a.seq()] == b[b.seq()]); + BEAST_EXPECT(mismatch(a,b) == a.seq() + 1); + BEAST_EXPECT(mismatch(b,a) == a.seq() + 1); + } + else + { + BEAST_EXPECT(a[a.seq()] != b[b.seq()]); + BEAST_EXPECT(mismatch(a,b) == diverge + 1); + BEAST_EXPECT(mismatch(b,a) == diverge + 1); + } + } + } + // Different chains, different seqs + { + // Compare around the divergence point + RCLValidatedLedger a{history[diverge], j}; + for(Seq offset = diverge/2; offset < 3*diverge/2; ++offset) + { + RCLValidatedLedger b{altHistory[offset-1], j}; + if(offset <= diverge) + { + BEAST_EXPECT(mismatch(a,b) == b.seq() + 1); + } + else + { + BEAST_EXPECT(mismatch(a,b) == diverge + 1); + } + } + } + + + } +}; + +BEAST_DEFINE_TESTSUITE(RCLValidations, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/consensus/Consensus_test.cpp b/src/test/consensus/Consensus_test.cpp index fb31ed4e3c8..93bf57bd076 100644 --- a/src/test/consensus/Consensus_test.cpp +++ b/src/test/consensus/Consensus_test.cpp @@ -512,8 +512,7 @@ class Consensus_test : public beast::unit_test::suite 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)); + BEAST_EXPECT(!jump.to.isAncestor(jump.from)); } } // fully validated jump forward in same chain @@ -525,8 +524,7 @@ class Consensus_test : public beast::unit_test::suite 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(jump.to.isAncestor(jump.from)); } } } @@ -825,6 +823,168 @@ class Consensus_test : public beast::unit_test::suite BEAST_EXPECT(sim.synchronized()); } + + // Helper collector for testPreferredByBranch + // Invasively disconnects network at bad times to cause splits + struct Disruptor + { + csf::PeerGroup& network; + csf::PeerGroup& groupCfast; + csf::PeerGroup& groupCsplit; + csf::SimDuration delay; + bool reconnected = false; + + Disruptor( + csf::PeerGroup& net, + csf::PeerGroup& c, + csf::PeerGroup& split, + csf::SimDuration d) + : network(net), groupCfast(c), groupCsplit(split), delay(d) + { + } + + template + void + on(csf::PeerID, csf::SimTime, E const&) + { + } + + + void + on(csf::PeerID who, csf::SimTime, csf::FullyValidateLedger const& e) + { + using namespace std::chrono; + // As soon as the the fastC node fully validates C, disconnect + // ALL c nodes from the network. The fast C node needs to disconnect + // as well to prevent it from relaying the validations it did see + if (who == groupCfast[0]->id && + e.ledger.seq() == csf::Ledger::Seq{2}) + { + network.disconnect(groupCsplit); + network.disconnect(groupCfast); + } + } + + void + on(csf::PeerID who, csf::SimTime, csf::AcceptLedger const& e) + { + // As soon as anyone generates a child of B or C, reconnect the + // network so those validations make it through + if (!reconnected && e.ledger.seq() == csf::Ledger::Seq{3}) + { + reconnected = true; + network.connect(groupCsplit, delay); + } + } + + + }; + + void + testPreferredByBranch() + { + using namespace csf; + using namespace std::chrono; + + // Simulate network splits that are prevented from forking when using + // preferred ledger by trie. This is a contrived example that involves + // excessive network splits, but demonstrates the safety improvement + // from the preferred ledger by trie approach. + + // Consider 10 validating nodes that comprise a single common UNL + // Ledger history: + // 1: A + // _/ \_ + // 2: B C + // _/ _/ \_ + // 3: D C' |||||||| (8 different ledgers) + + // - All nodes generate the common ledger A + // - 2 nodes generate B and 8 nodes generate C + // - Only 1 of the C nodes sees all the C validations and fully + // validates C. The rest of the C nodes split at just the right time + // such that they never see any C validations but their own. + // - The C nodes continue and generate 8 different child ledgers. + // - Meanwhile, the D nodes only saw 1 validation for C and 2 validations + // for B. + // - The network reconnects and the validations for generation 3 ledgers + // are observed (D and the 8 C's) + // - In the old approach, 2 votes for D outweights 1 vote for each C' + // so the network would avalanche towards D and fully validate it + // EVEN though C was fully validated by one node + // - In the new approach, 2 votes for D are not enough to outweight the + // 8 implicit votes for C, so nodes will avalanche to C instead + + + ConsensusParms const parms{}; + Sim sim; + + // Goes A->B->D + PeerGroup groupABD = sim.createGroup(2); + // Single node that initially fully validates C before the split + PeerGroup groupCfast = sim.createGroup(1); + // Generates C, but fails to fully validate before the split + PeerGroup groupCsplit = sim.createGroup(7); + + PeerGroup groupNotFastC = groupABD + groupCsplit; + PeerGroup network = groupABD + groupCsplit + groupCfast; + + SimDuration delay = round(0.2 * parms.ledgerGRANULARITY); + SimDuration fDelay = round(0.1 * parms.ledgerGRANULARITY); + + network.trust(network); + // C must have a shorter delay to see all the validations before the + // other nodes + network.connect(groupCfast, fDelay); + // The rest of the network is connected at the same speed + groupNotFastC.connect(groupNotFastC, delay); + + Disruptor dc(network, groupCfast, groupCsplit, delay); + sim.collectors.add(dc); + + // Consensus round to generate ledger A + sim.run(1); + BEAST_EXPECT(sim.synchronized()); + + // Next round generates B and C + // To force B, we inject an extra transaction in to those nodes + for(Peer * peer : groupABD) + { + peer->txInjections.emplace( + peer->lastClosedLedger.seq(), Tx{42}); + } + // The Disruptor will ensure that nodes disconnect before the C + // validations make it to all but the fastC node + sim.run(1); + + // We are no longer in sync, but have not yet forked: + // 9 nodes consider A the last fully validated ledger and fastC sees C + BEAST_EXPECT(!sim.synchronized()); + BEAST_EXPECT(sim.branches() == 1); + + // Run another round to generate the 8 different C' ledgers + for (Peer * p : network) + p->submit(Tx(static_cast(p->id))); + sim.run(1); + + // Still not forked + BEAST_EXPECT(!sim.synchronized()); + BEAST_EXPECT(sim.branches() == 1); + + // Disruptor will reconnect all but the fastC node + sim.run(1); + + if(BEAST_EXPECT(sim.branches() == 1)) + { + BEAST_EXPECT(sim.synchronized()); + } + else // old approach caused a fork + { + BEAST_EXPECT(sim.branches(groupNotFastC) == 1); + BEAST_EXPECT(sim.synchronized(groupNotFastC) == 1); + } + } + void run() override { @@ -839,6 +999,7 @@ class Consensus_test : public beast::unit_test::suite testConsensusCloseTimeRounding(); testFork(); testHubNetwork(); + testPreferredByBranch(); } }; diff --git a/src/test/consensus/LedgerTrie_test.cpp b/src/test/consensus/LedgerTrie_test.cpp new file mode 100644 index 00000000000..651bf10383d --- /dev/null +++ b/src/test/consensus/LedgerTrie_test.cpp @@ -0,0 +1,663 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +namespace ripple { +namespace test { + +class LedgerTrie_test : public beast::unit_test::suite +{ + beast::Journal j; + + + void + testInsert() + { + using namespace csf; + // Single entry by itself + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 1); + + t.insert(h["abc"]); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 2); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); + } + // Suffix of existing (extending tree) + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + BEAST_EXPECT(t.checkInvariants()); + // extend with no siblings + t.insert(h["abcd"]); + BEAST_EXPECT(t.checkInvariants()); + + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); + BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1); + + // extend with existing sibling + t.insert(h["abce"]); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 3); + BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1); + BEAST_EXPECT(t.tipSupport(h["abce"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abce"]) == 1); + } + // uncommitted of existing node + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abcd"]); + BEAST_EXPECT(t.checkInvariants()); + // uncommitted with no siblings + t.insert(h["abcdf"]); + BEAST_EXPECT(t.checkInvariants()); + + BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcd"]) == 2); + BEAST_EXPECT(t.tipSupport(h["abcdf"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcdf"]) == 1); + + // uncommitted with existing child + t.insert(h["abc"]); + BEAST_EXPECT(t.checkInvariants()); + + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 3); + BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcd"]) == 2); + BEAST_EXPECT(t.tipSupport(h["abcdf"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcdf"]) == 1); + } + // Suffix + uncommitted of existing node + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abcd"]); + BEAST_EXPECT(t.checkInvariants()); + t.insert(h["abce"]); + BEAST_EXPECT(t.checkInvariants()); + + BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); + BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1); + BEAST_EXPECT(t.tipSupport(h["abce"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abce"]) == 1); + } + // Suffix + uncommitted with existing child + { + // abcd : abcde, abcf + + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abcd"]); + BEAST_EXPECT(t.checkInvariants()); + t.insert(h["abcde"]); + BEAST_EXPECT(t.checkInvariants()); + t.insert(h["abcf"]); + BEAST_EXPECT(t.checkInvariants()); + + BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 3); + BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcd"]) == 2); + BEAST_EXPECT(t.tipSupport(h["abcf"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcf"]) == 1); + BEAST_EXPECT(t.tipSupport(h["abcde"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcde"]) == 1); + } + + // Multiple counts + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"],4); + BEAST_EXPECT(t.tipSupport(h["ab"]) == 4); + BEAST_EXPECT(t.branchSupport(h["ab"]) == 4); + BEAST_EXPECT(t.tipSupport(h["a"]) == 0); + BEAST_EXPECT(t.branchSupport(h["a"]) == 4); + + t.insert(h["abc"],2); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 2); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); + BEAST_EXPECT(t.tipSupport(h["ab"]) == 4); + BEAST_EXPECT(t.branchSupport(h["ab"]) == 6); + BEAST_EXPECT(t.tipSupport(h["a"]) == 0); + BEAST_EXPECT(t.branchSupport(h["a"]) == 6); + + } + } + + void + testRemove() + { + using namespace csf; + // Not in trie + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + + BEAST_EXPECT(!t.remove(h["ab"])); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(!t.remove(h["a"])); + BEAST_EXPECT(t.checkInvariants()); + } + // In trie but with 0 tip support + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abcd"]); + t.insert(h["abce"]); + + BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); + BEAST_EXPECT(!t.remove(h["abc"])); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); + } + // In trie with > 1 tip support + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"],2); + + BEAST_EXPECT(t.tipSupport(h["abc"]) == 2); + BEAST_EXPECT(t.remove(h["abc"])); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + + t.insert(h["abc"], 1); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 2); + BEAST_EXPECT(t.remove(h["abc"], 2)); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); + + t.insert(h["abc"], 3); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 3); + BEAST_EXPECT(t.remove(h["abc"], 300)); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); + + } + // In trie with = 1 tip support, no children + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"]); + t.insert(h["abc"]); + + BEAST_EXPECT(t.tipSupport(h["ab"]) == 1); + BEAST_EXPECT(t.branchSupport(h["ab"]) == 2); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 1); + + BEAST_EXPECT(t.remove(h["abc"])); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.tipSupport(h["ab"]) == 1); + BEAST_EXPECT(t.branchSupport(h["ab"]) == 1); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 0); + } + // In trie with = 1 tip support, 1 child + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"]); + t.insert(h["abc"]); + t.insert(h["abcd"]); + + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); + BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1); + + BEAST_EXPECT(t.remove(h["abc"])); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 1); + BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1); + } + // In trie with = 1 tip support, > 1 children + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"]); + t.insert(h["abc"]); + t.insert(h["abcd"]); + t.insert(h["abce"]); + + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 3); + + BEAST_EXPECT(t.remove(h["abc"])); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); + } + + // In trie with = 1 tip support, parent compaction + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"]); + t.insert(h["abc"]); + t.insert(h["abd"]); + BEAST_EXPECT(t.checkInvariants()); + t.remove(h["ab"]); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.tipSupport(h["abd"]) == 1); + BEAST_EXPECT(t.tipSupport(h["ab"]) == 0); + BEAST_EXPECT(t.branchSupport(h["ab"]) == 2); + + t.remove(h["abd"]); + BEAST_EXPECT(t.checkInvariants()); + + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.branchSupport(h["ab"]) == 1); + + } + } + + void + testSupport() + { + using namespace csf; + using Seq = Ledger::Seq; + + + LedgerTrie t; + LedgerHistoryHelper h; + BEAST_EXPECT(t.tipSupport(h["a"]) == 0); + BEAST_EXPECT(t.tipSupport(h["axy"]) == 0); + + BEAST_EXPECT(t.branchSupport(h["a"]) == 0); + BEAST_EXPECT(t.branchSupport(h["axy"]) == 0); + + t.insert(h["abc"]); + BEAST_EXPECT(t.tipSupport(h["a"]) == 0); + BEAST_EXPECT(t.tipSupport(h["ab"]) == 0); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.tipSupport(h["abcd"]) == 0); + + BEAST_EXPECT(t.branchSupport(h["a"]) == 1); + BEAST_EXPECT(t.branchSupport(h["ab"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abcd"]) == 0); + + t.insert(h["abe"]); + BEAST_EXPECT(t.tipSupport(h["a"]) == 0); + BEAST_EXPECT(t.tipSupport(h["ab"]) == 0); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); + BEAST_EXPECT(t.tipSupport(h["abe"]) == 1); + + BEAST_EXPECT(t.branchSupport(h["a"]) == 2); + BEAST_EXPECT(t.branchSupport(h["ab"]) == 2); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abe"]) == 1); + + t.remove(h["abc"]); + BEAST_EXPECT(t.tipSupport(h["a"]) == 0); + BEAST_EXPECT(t.tipSupport(h["ab"]) == 0); + BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); + BEAST_EXPECT(t.tipSupport(h["abe"]) == 1); + + BEAST_EXPECT(t.branchSupport(h["a"]) == 1); + BEAST_EXPECT(t.branchSupport(h["ab"]) == 1); + BEAST_EXPECT(t.branchSupport(h["abc"]) == 0); + BEAST_EXPECT(t.branchSupport(h["abe"]) == 1); + + } + + void + testGetPreferred() + { + using namespace csf; + using Seq = Ledger::Seq; + // Empty + { + LedgerTrie t; + LedgerHistoryHelper h; + BEAST_EXPECT(t.getPreferred(Seq{0}).id == h[""].id()); + BEAST_EXPECT(t.getPreferred(Seq{2}).id == h[""].id()); + } + // Single node no children + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abc"].id()); + } + // Single node smaller child support + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"]); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abc"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abc"].id()); + } + // Single node larger child + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"],2); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abcd"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abcd"].id()); + } + // Single node smaller children support + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"]); + t.insert(h["abce"]); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abc"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abc"].id()); + + t.insert(h["abc"]); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abc"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abc"].id()); + } + // Single node larger children + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"],2); + t.insert(h["abce"]); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abc"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abc"].id()); + + t.insert(h["abcd"]); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abcd"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abcd"].id()); + } + // Tie-breaker by id + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abcd"],2); + t.insert(h["abce"],2); + + BEAST_EXPECT(h["abce"].id() > h["abcd"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abce"].id()); + + t.insert(h["abcd"]); + BEAST_EXPECT(h["abce"].id() > h["abcd"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abcd"].id()); + } + + // Tie-breaker not needed + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"]); + t.insert(h["abce"],2); + // abce only has a margin of 1, but it owns the tie-breaker + BEAST_EXPECT(h["abce"].id() > h["abcd"].id()); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abce"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abce"].id()); + + // Switch support from abce to abcd, tie-breaker now needed + t.remove(h["abce"]); + t.insert(h["abcd"]); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abc"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abc"].id()); + } + + // Single node larger grand child + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"],2); + t.insert(h["abcde"],4); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abcde"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abcde"].id()); + BEAST_EXPECT(t.getPreferred(Seq{5}).id == h["abcde"].id()); + } + + // Too much uncommitted support from competing branches + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcde"],2); + t.insert(h["abcfg"],2); + // 'de' and 'fg' are tied without 'abc' vote + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abc"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abc"].id()); + BEAST_EXPECT(t.getPreferred(Seq{5}).id == h["abc"].id()); + + t.remove(h["abc"]); + t.insert(h["abcd"]); + + // 'de' branch has 3 votes to 2, so earlier sequences see it as + // preferred + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abcde"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["abcde"].id()); + + // However, if you validated a ledger with Seq 5, potentially on + // a different branch, you do not yet know if they chose abcd + // or abcf because of you, so abc remains preferred + BEAST_EXPECT(t.getPreferred(Seq{5}).id == h["abc"].id()); + } + + // Changing largestSeq perspective changes preferred branch + { + /** Build the tree below with initial tip support annotated + A + / \ + B(1) C(1) + / | | + H D F(1) + | + E(2) + | + G + */ + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"]); + t.insert(h["ac"]); + t.insert(h["acf"]); + t.insert(h["abde"],2); + + // B has more branch support + BEAST_EXPECT(t.getPreferred(Seq{1}).id == h["ab"].id()); + BEAST_EXPECT(t.getPreferred(Seq{2}).id == h["ab"].id()); + // But if you last validated D,F or E, you do not yet know + // if someone used that validation to commit to B or C + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["a"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["a"].id()); + + /** One of E advancing to G doesn't change anything + A + / \ + B(1) C(1) + / | | + H D F(1) + | + E(1) + | + G(1) + */ + t.remove(h["abde"]); + t.insert(h["abdeg"]); + + BEAST_EXPECT(t.getPreferred(Seq{1}).id == h["ab"].id()); + BEAST_EXPECT(t.getPreferred(Seq{2}).id == h["ab"].id()); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["a"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["a"].id()); + BEAST_EXPECT(t.getPreferred(Seq{5}).id == h["a"].id()); + + /** C advancing to H does advance the seq 3 preferred ledger + A + / \ + B(1) C + / | | + H(1)D F(1) + | + E(1) + | + G(1) + */ + t.remove(h["ac"]); + t.insert(h["abh"]); + BEAST_EXPECT(t.getPreferred(Seq{1}).id == h["ab"].id()); + BEAST_EXPECT(t.getPreferred(Seq{2}).id == h["ab"].id()); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["ab"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["a"].id()); + BEAST_EXPECT(t.getPreferred(Seq{5}).id == h["a"].id()); + + /** F advancing to E also moves the preferred ledger forward + A + / \ + B(1) C + / | | + H(1)D F + | + E(2) + | + G(1) + */ + t.remove(h["acf"]); + t.insert(h["abde"]); + BEAST_EXPECT(t.getPreferred(Seq{1}).id == h["abde"].id()); + BEAST_EXPECT(t.getPreferred(Seq{2}).id == h["abde"].id()); + BEAST_EXPECT(t.getPreferred(Seq{3}).id == h["abde"].id()); + BEAST_EXPECT(t.getPreferred(Seq{4}).id == h["ab"].id()); + BEAST_EXPECT(t.getPreferred(Seq{5}).id == h["ab"].id()); + + } + + + } + + void + testRootRelated() + { + using namespace csf; + using Seq = Ledger::Seq; + // Since the root is a special node that breaks the no-single child + // invariant, do some tests that exercise it. + + LedgerTrie t; + LedgerHistoryHelper h; + BEAST_EXPECT(!t.remove(h[""])); + BEAST_EXPECT(t.branchSupport(h[""]) == 0); + BEAST_EXPECT(t.tipSupport(h[""]) == 0); + + t.insert(h["a"]); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.branchSupport(h[""]) == 1); + BEAST_EXPECT(t.tipSupport(h[""]) == 0); + + t.insert(h["e"]); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.branchSupport(h[""]) == 2); + BEAST_EXPECT(t.tipSupport(h[""]) == 0); + + BEAST_EXPECT(t.remove(h["e"])); + BEAST_EXPECT(t.checkInvariants()); + BEAST_EXPECT(t.branchSupport(h[""]) == 1); + BEAST_EXPECT(t.tipSupport(h[""]) == 0); + } + + void + testStress() + { + using namespace csf; + LedgerTrie t; + LedgerHistoryHelper h; + + // Test quasi-randomly add/remove supporting for different ledgers + // from a branching history. + + // Ledgers have sequence 1,2,3,4 + std::uint32_t const depth = 4; + // Each ledger has 4 possible children + std::uint32_t const width = 4; + + std::uint32_t const iterations = 10000; + + // Use explicit seed to have same results for CI + std::mt19937 gen{ 42 }; + std::uniform_int_distribution<> depthDist(0, depth-1); + std::uniform_int_distribution<> widthDist(0, width-1); + std::uniform_int_distribution<> flip(0, 1); + for(std::uint32_t i = 0; i < iterations; ++i) + { + // pick a random ledger history + std::string curr = ""; + char depth = depthDist(gen); + char offset = 0; + for(char d = 0; d < depth; ++d) + { + char a = offset + widthDist(gen); + curr += a; + offset = (a + 1) * width; + } + + // 50-50 to add remove + if(flip(gen) == 0) + t.insert(h[curr]); + else + t.remove(h[curr]); + if(!BEAST_EXPECT(t.checkInvariants())) + return; + } + } + + void + run() + { + testInsert(); + testRemove(); + testSupport(); + testGetPreferred(); + testRootRelated(); + testStress(); + } +}; + +BEAST_DEFINE_TESTSUITE(LedgerTrie, consensus, ripple); +} // namespace test +} // namespace ripple diff --git a/src/test/consensus/Validations_test.cpp b/src/test/consensus/Validations_test.cpp index 0f31210c681..24821fd47d3 100644 --- a/src/test/consensus/Validations_test.cpp +++ b/src/test/consensus/Validations_test.cpp @@ -23,10 +23,10 @@ #include #include +#include #include #include #include -#include namespace ripple { namespace test { @@ -112,24 +112,58 @@ 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(Ledger::Seq seq, - Ledger::ID i, + validate( + Ledger::ID id, + Ledger::Seq seq, + NetClock::duration signOffset, + NetClock::duration seenOffset, + bool full) const + { + Validation v{id, + seq, + now() + signOffset, + now() + seenOffset, + currKey(), + nodeID_, + full, + loadFee_}; + if (trusted_) + v.setTrusted(); + return v; + } + + Validation + validate( + Ledger ledger, NetClock::duration signOffset, NetClock::duration seenOffset) const { - return Validation{i, seq, now() + signOffset, now() + seenOffset, - currKey(), nodeID_, trusted_, loadFee_}; + return validate( + ledger.id(), ledger.seq(), signOffset, seenOffset, true); } - // Issue a new validation with the given sequence number and id Validation - validation(Ledger::Seq seq, Ledger::ID i) const + validate(Ledger ledger) const { - return validation( - seq, i, NetClock::duration{0}, NetClock::duration{0}); + return validate( + ledger.id(), + ledger.seq(), + NetClock::duration{0}, + NetClock::duration{0}, + true); } - }; + Validation + partial(Ledger ledger) const + { + return validate( + ledger.id(), + ledger.seq(), + NetClock::duration{0}, + NetClock::duration{0}, + false); + } + }; // Saved StaleData for inspection in test struct StaleData @@ -138,15 +172,34 @@ class Validations_test : public beast::unit_test::suite hash_map flushed; }; - // Generic Validations policy that saves stale/flushed data into + // Generic Validations adaptor that saves stale/flushed data into // a StaleData instance. - class StalePolicy + class Adaptor { StaleData& staleData_; clock_type& c_; + LedgerOracle& oracle_; public: - StalePolicy(StaleData& sd, clock_type& c) : staleData_{sd}, c_{c} + // Non-locking mutex to avoid locks in generic Validations + struct Mutex + { + void + lock() + { + } + + void + unlock() + { + } + }; + + using Validation = csf::Validation; + using Ledger = csf::Ledger; + + Adaptor(StaleData& sd, clock_type& c, LedgerOracle& o) + : staleData_{sd}, c_{c}, oracle_{o} { } @@ -167,27 +220,16 @@ class Validations_test : public beast::unit_test::suite { staleData_.flushed = std::move(remaining); } - }; - - // Non-locking mutex to avoid locks in generic Validations - struct NotAMutex - { - void - lock() - { - } - void - unlock() + boost::optional + acquire(Ledger::ID const& id) { + return oracle_.lookup(id); } }; // Specialize generic Validations using the above types - using TestValidations = Validations; - - // Hoist enum for writing simpler tests - using AddOutcome = TestValidations::AddOutcome; + using TestValidations = Validations; // Gather the dependencies of TestValidations in a single class and provide // accessors for simplifying test logic @@ -196,28 +238,20 @@ class Validations_test : public beast::unit_test::suite StaleData staleData_; ValidationParms p_; beast::manual_clock clock_; - beast::Journal j_; TestValidations tv_; PeerID nextNodeId_{0}; public: - TestHarness() : tv_(p_, clock_, j_, staleData_, clock_) - { - } - - // Helper to add an existing validation - AddOutcome - add(Node const& n, Validation const& v) + TestHarness(LedgerOracle& o) + : tv_(p_, clock_, staleData_, clock_, o) { - return tv_.add(n.masterKey(), v); } - // Helper to directly create the validation - template - std::enable_if_t<(sizeof...(Ts) > 1), AddOutcome> - add(Node const& n, Ts&&... ts) + ValStatus + add(Validation const& v) { - return add(n, n.validation(std::forward(ts)...)); + PeerKey masterKey{v.nodeID(), 0}; + return tv_.add(masterKey, v); } TestValidations& @@ -257,140 +291,157 @@ class Validations_test : public beast::unit_test::suite } }; + Ledger const genesisLedger{Ledger::MakeGenesis{}}; + void testAddValidation() { - // Test adding current,stale,repeat,sameSeq validations using namespace std::chrono_literals; - TestHarness harness; - Node a = harness.makeNode(); + testcase("Add validation"); + LedgerHistoryHelper h; + Ledger ledgerA = h["a"]; + Ledger ledgerAB = h["ab"]; + Ledger ledgerAZ = h["az"]; + Ledger ledgerABC = h["abc"]; + Ledger ledgerABCD = h["abcd"]; + Ledger ledgerABCDE = h["abcde"]; + { - { - auto const v = a.validation(Ledger::Seq{1}, Ledger::ID{1}); + TestHarness harness(h.oracle); + Node n = harness.makeNode(); - // Add a current validation - BEAST_EXPECT(AddOutcome::current == harness.add(a, v)); + auto const v = n.validate(ledgerA); - // Re-adding is repeat - BEAST_EXPECT(AddOutcome::repeat == harness.add(a, v)); - } + // Add a current validation + BEAST_EXPECT(ValStatus::current == harness.add(v)); - { - harness.clock().advance(1s); - // Replace with a new validation and ensure the old one is stale - BEAST_EXPECT(harness.stale().empty()); + // Re-adding violates the increasing seq requirement for full + // validations + BEAST_EXPECT(ValStatus::badSeq == harness.add(v)); - BEAST_EXPECT(AddOutcome::current == - harness.add(a, Ledger::Seq{2}, Ledger::ID{2})); + harness.clock().advance(1s); + // Replace with a new validation and ensure the old one is stale + BEAST_EXPECT(harness.stale().empty()); - BEAST_EXPECT(harness.stale().size() == 1); + BEAST_EXPECT( + ValStatus::current == harness.add(n.validate(ledgerAB))); - BEAST_EXPECT(harness.stale()[0].ledgerID() == Ledger::ID{1}); - } + BEAST_EXPECT(harness.stale().size() == 1); - { - // Test the node changing signing key, then reissuing a ledger + BEAST_EXPECT(harness.stale()[0].ledgerID() == ledgerA.id()); - // Confirm old ledger on hand, but not new ledger - BEAST_EXPECT( - harness.vals().numTrustedForLedger(Ledger::ID{2}) == 1); - BEAST_EXPECT( - harness.vals().numTrustedForLedger(Ledger::ID{20}) == 0); + // Test the node changing signing key - // Issue a new signing key and re-issue the validation with a - // new ID but the same sequence number - a.advanceKey(); + // Confirm old ledger on hand, but not new ledger + BEAST_EXPECT( + harness.vals().numTrustedForLedger(ledgerAB.id()) == 1); + BEAST_EXPECT( + harness.vals().numTrustedForLedger(ledgerABC.id()) == 0); - // No validations following ID{2} - BEAST_EXPECT(harness.vals().getNodesAfter(Ledger::ID{2}) == 0); + // Rotate signing keys + n.advanceKey(); - BEAST_EXPECT( - AddOutcome::sameSeq == harness.add(a, Ledger::Seq{2}, Ledger::ID{20})); + harness.clock().advance(1s); - // Old ID should be gone ... - 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(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(Ledger::ID{2}) == 1); + // Cannot re-do the same full validation sequence + BEAST_EXPECT( + ValStatus::badSeq == harness.add(n.validate(ledgerAB))); + // Cannot send the same partial validation sequence + BEAST_EXPECT( + ValStatus::badSeq == harness.add(n.partial(ledgerAB))); - } + // Now trusts the newest ledger too + harness.clock().advance(1s); + BEAST_EXPECT( + ValStatus::current == harness.add(n.validate(ledgerABC))); + BEAST_EXPECT( + harness.vals().numTrustedForLedger(ledgerAB.id()) == 1); + BEAST_EXPECT( + harness.vals().numTrustedForLedger(ledgerABC.id()) == 1); - // A new key, but re-issue a validation with the same ID and - // Sequence - a.advanceKey(); + // Processing validations out of order should ignore the older + // validation + harness.clock().advance(2s); + auto const valABCDE = n.validate(ledgerABCDE); - BEAST_EXPECT( - AddOutcome::sameSeq == harness.add(a, Ledger::Seq{2}, Ledger::ID{20})); - { - // Still the only trusted validation for ID{20} - auto trustedVals = - 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(Ledger::ID{2}) == 1); - } - } + harness.clock().advance(4s); + auto const valABCD = n.validate(ledgerABCD); - { - // Processing validations out of order should ignore the older - harness.clock().advance(2s); - auto const val3 = a.validation(Ledger::Seq{3}, Ledger::ID{3}); + BEAST_EXPECT(ValStatus::current == harness.add(valABCD)); - harness.clock().advance(4s); - auto const val4 = a.validation(Ledger::Seq{4}, Ledger::ID{4}); + BEAST_EXPECT(ValStatus::stale == harness.add(valABCDE)); + } - BEAST_EXPECT(AddOutcome::current == harness.add(a, val4)); + { + // Process validations out of order with shifted times - BEAST_EXPECT(AddOutcome::stale == harness.add(a, val3)); + TestHarness harness(h.oracle); + Node n = harness.makeNode(); - // re-issued should not be added - auto const val4reissue = - a.validation(Ledger::Seq{4}, Ledger::ID{44}); + // Establish a new current validation + BEAST_EXPECT( + ValStatus::current == harness.add(n.validate(ledgerA))); - BEAST_EXPECT(AddOutcome::stale == harness.add(a, val4reissue)); - } - { - // Process validations out of order with shifted times + // Process a validation that has "later" seq but early sign time + BEAST_EXPECT( + ValStatus::stale == + harness.add(n.validate(ledgerAB, -1s, -1s))); - // flush old validations - harness.clock().advance(1h); + // Process a validation that has a later seq and later sign + // time + BEAST_EXPECT( + ValStatus::current == + harness.add(n.validate(ledgerABC, 1s, 1s))); + } - // Establish a new current validation - BEAST_EXPECT(AddOutcome::current == - harness.add(a, Ledger::Seq{8}, Ledger::ID{8})); + { + // Test stale on arrival validations + TestHarness harness(h.oracle); + Node n = harness.makeNode(); - // Process a validation that has "later" seq but early sign time - BEAST_EXPECT(AddOutcome::stale == - harness.add(a, Ledger::Seq{9}, Ledger::ID{9}, -1s, -1s)); + BEAST_EXPECT( + ValStatus::stale == + harness.add(n.validate( + ledgerA, -harness.parms().validationCURRENT_EARLY, 0s))); - // 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( + ValStatus::stale == + harness.add(n.validate( + ledgerA, harness.parms().validationCURRENT_WALL, 0s))); - BEAST_EXPECT(AddOutcome::stale == - harness.add(a, Ledger::Seq{15}, Ledger::ID{15}, - -harness.parms().validationCURRENT_EARLY, 0s)); + BEAST_EXPECT( + ValStatus::stale == + harness.add(n.validate( + ledgerA, 0s, harness.parms().validationCURRENT_LOCAL))); + } + + { + // Test that full or partials cannot be sent for older sequence + // numbers, unless time-out has happened + for (bool doFull : {true, false}) + { + TestHarness harness(h.oracle); + Node n = harness.makeNode(); - BEAST_EXPECT(AddOutcome::stale == - harness.add(a, Ledger::Seq{15}, Ledger::ID{15}, - harness.parms().validationCURRENT_WALL, 0s)); + auto process = [&](Ledger & lgr) + { + if(doFull) + return harness.add(n.validate(lgr)); + return harness.add(n.partial(lgr)); + }; - BEAST_EXPECT(AddOutcome::stale == - harness.add(a, Ledger::Seq{15}, Ledger::ID{15}, 0s, - harness.parms().validationCURRENT_LOCAL)); + BEAST_EXPECT(ValStatus::current == process(ledgerABC)); + harness.clock().advance(1s); + BEAST_EXPECT(ledgerAB.seq() < ledgerABC.seq()); + BEAST_EXPECT(ValStatus::badSeq == process(ledgerAB)); + + // If we advance far enough for AB to expire, we can fully + // validate or partially validate that sequence number again + BEAST_EXPECT(ValStatus::badSeq == process(ledgerAZ)); + harness.clock().advance( + harness.parms().validationSET_EXPIRES + 1ms); + BEAST_EXPECT(ValStatus::current == process(ledgerAZ)); } } } @@ -398,97 +449,142 @@ class Validations_test : public beast::unit_test::suite void testOnStale() { - // Verify validation becomes stale based solely on time passing - TestHarness harness; - Node a = harness.makeNode(); + testcase("Stale validation"); + // Verify validation becomes stale based solely on time passing, but + // use different functions to trigger the check for staleness - 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); + LedgerHistoryHelper h; + Ledger ledgerA = h["a"]; + Ledger ledgerAB = h["ab"]; - // trigger iteration over current - harness.vals().currentTrusted(); - BEAST_EXPECT(harness.stale().size() == 1); - BEAST_EXPECT(harness.stale()[0].ledgerID() == Ledger::ID{1}); + using Trigger = std::function; + + std::vector triggers = { + [&](TestValidations& vals) { vals.currentTrusted(); }, + [&](TestValidations& vals) { vals.getCurrentPublicKeys(); }, + [&](TestValidations& vals) { vals.getPreferred(genesisLedger); }, + [&](TestValidations& vals) { + vals.getNodesAfter(ledgerA, ledgerA.id()); + }}; + for (Trigger trigger : triggers) + { + TestHarness harness(h.oracle); + Node n = harness.makeNode(); + + BEAST_EXPECT( + ValStatus::current == harness.add(n.validate(ledgerAB))); + trigger(harness.vals()); + BEAST_EXPECT( + harness.vals().getNodesAfter(ledgerA, ledgerA.id()) == 1); + BEAST_EXPECT( + harness.vals().getPreferred(genesisLedger) == + std::make_pair(ledgerAB.seq(), ledgerAB.id())); + BEAST_EXPECT(harness.stale().empty()); + harness.clock().advance(harness.parms().validationCURRENT_LOCAL); + + // trigger check for stale + trigger(harness.vals()); + + BEAST_EXPECT(harness.stale().size() == 1); + BEAST_EXPECT(harness.stale()[0].ledgerID() == ledgerAB.id()); + BEAST_EXPECT( + harness.vals().getNodesAfter(ledgerA, ledgerA.id()) == 0); + BEAST_EXPECT( + harness.vals().getPreferred(genesisLedger) == + std::make_pair(Ledger::Seq{0}, Ledger::ID{0})); + } } void testGetNodesAfter() { - // Test getting number of nodes working on a validation following - // a prescribed one + // Test getting number of nodes working on a validation descending + // a prescribed one. This count should only be for trusted nodes, but + // includes partial and full validations + using namespace std::chrono_literals; + testcase("Get nodes after"); - TestHarness harness; + LedgerHistoryHelper h; + Ledger ledgerA = h["a"]; + Ledger ledgerAB = h["ab"]; + Ledger ledgerABC = h["abc"]; + Ledger ledgerAD = h["ad"]; + + TestHarness harness(h.oracle); Node a = harness.makeNode(), b = harness.makeNode(), c = harness.makeNode(), d = harness.makeNode(); - c.untrust(); - // first round a,b,c agree, d has differing id - for (auto const& node : {a, b, c}) - 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})); + // first round a,b,c agree, d has is partial + BEAST_EXPECT(ValStatus::current == harness.add(a.validate(ledgerA))); + BEAST_EXPECT(ValStatus::current == harness.add(b.validate(ledgerA))); + BEAST_EXPECT(ValStatus::current == harness.add(c.validate(ledgerA))); + BEAST_EXPECT(ValStatus::current == harness.add(d.partial(ledgerA))); - // Nothing past ledger 1 yet - BEAST_EXPECT(harness.vals().getNodesAfter(Ledger::ID{1}) == 0); + for (Ledger const& ledger : {ledgerA, ledgerAB, ledgerABC, ledgerAD}) + BEAST_EXPECT( + harness.vals().getNodesAfter(ledger, ledger.id()) == 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, 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); + BEAST_EXPECT(ValStatus::current == harness.add(a.validate(ledgerAB))); + BEAST_EXPECT(ValStatus::current == harness.add(b.validate(ledgerABC))); + BEAST_EXPECT(ValStatus::current == harness.add(c.validate(ledgerAB))); + BEAST_EXPECT(ValStatus::current == harness.add(d.partial(ledgerABC))); + + BEAST_EXPECT(harness.vals().getNodesAfter(ledgerA, ledgerA.id()) == 3); + BEAST_EXPECT( + harness.vals().getNodesAfter(ledgerAB, ledgerAB.id()) == 2); + BEAST_EXPECT( + harness.vals().getNodesAfter(ledgerABC, ledgerABC.id()) == 0); + BEAST_EXPECT( + harness.vals().getNodesAfter(ledgerAD, ledgerAD.id()) == 0); + + // If given a ledger inconsistent with the id, is still able to check + // using slower method + BEAST_EXPECT(harness.vals().getNodesAfter(ledgerAD, ledgerA.id()) == 1); + BEAST_EXPECT( + harness.vals().getNodesAfter(ledgerAD, ledgerAB.id()) == 2); } void testCurrentTrusted() { - // Test getting current trusted validations using namespace std::chrono_literals; + testcase("Current trusted validations"); - TestHarness harness; + LedgerHistoryHelper h; + Ledger ledgerA = h["a"]; + Ledger ledgerB = h["b"]; + Ledger ledgerAC = h["ac"]; + + TestHarness harness(h.oracle); Node a = harness.makeNode(), b = harness.makeNode(); b.untrust(); - 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})); + BEAST_EXPECT(ValStatus::current == harness.add(a.validate(ledgerA))); + BEAST_EXPECT(ValStatus::current == harness.add(b.validate(ledgerB))); // Only a is trusted BEAST_EXPECT(harness.vals().currentTrusted().size() == 1); BEAST_EXPECT( - harness.vals().currentTrusted()[0].ledgerID() == Ledger::ID{1}); - BEAST_EXPECT( - harness.vals().currentTrusted()[0].seq() == Ledger::Seq{1}); + harness.vals().currentTrusted()[0].ledgerID() == ledgerA.id()); + BEAST_EXPECT(harness.vals().currentTrusted()[0].seq() == ledgerA.seq()); harness.clock().advance(3s); for (auto const& node : {a, b}) - BEAST_EXPECT(AddOutcome::current == - harness.add(node, Ledger::Seq{2}, Ledger::ID{2})); + BEAST_EXPECT( + ValStatus::current == harness.add(node.validate(ledgerAC))); // New validation for a BEAST_EXPECT(harness.vals().currentTrusted().size() == 1); BEAST_EXPECT( - harness.vals().currentTrusted()[0].ledgerID() == Ledger::ID{2}); + harness.vals().currentTrusted()[0].ledgerID() == ledgerAC.id()); BEAST_EXPECT( - harness.vals().currentTrusted()[0].seq() == Ledger::Seq{2}); + harness.vals().currentTrusted()[0].seq() == ledgerAC.seq()); // Pass enough time for it to go stale harness.clock().advance(harness.parms().validationCURRENT_LOCAL); @@ -498,36 +594,40 @@ class Validations_test : public beast::unit_test::suite void testGetCurrentPublicKeys() { - // Test getting current keys validations using namespace std::chrono_literals; + testcase("Current public keys"); + + LedgerHistoryHelper h; + Ledger ledgerA = h["a"]; + Ledger ledgerAC = h["ac"]; - TestHarness harness; + TestHarness harness(h.oracle); Node a = harness.makeNode(), b = harness.makeNode(); b.untrust(); for (auto const& node : {a, b}) - BEAST_EXPECT(AddOutcome::current == - harness.add(node, Ledger::Seq{1}, Ledger::ID{1})); + BEAST_EXPECT( + ValStatus::current == harness.add(node.validate(ledgerA))); { - hash_set const expectedKeys = { - a.masterKey(), b.masterKey()}; + hash_set const expectedKeys = {a.masterKey(), + b.masterKey()}; BEAST_EXPECT(harness.vals().getCurrentPublicKeys() == expectedKeys); } harness.clock().advance(3s); - // Change keys + // Change keys and issue partials a.advanceKey(); b.advanceKey(); for (auto const& node : {a, b}) - BEAST_EXPECT(AddOutcome::current == - harness.add(node, Ledger::Seq{2}, Ledger::ID{2})); + BEAST_EXPECT( + ValStatus::current == harness.add(node.partial(ledgerAC))); { - hash_set const expectedKeys = { - a.masterKey(), b.masterKey()}; + hash_set const expectedKeys = {a.masterKey(), + b.masterKey()}; BEAST_EXPECT(harness.vals().getCurrentPublicKeys() == expectedKeys); } @@ -536,98 +636,12 @@ class Validations_test : public beast::unit_test::suite BEAST_EXPECT(harness.vals().getCurrentPublicKeys().empty()); } - void - testCurrentTrustedDistribution() - { - // Test the trusted distribution calculation, including ledger slips - // and sequence cutoffs - using namespace std::chrono_literals; - - TestHarness harness; - - Node baby = harness.makeNode(), papa = harness.makeNode(), - mama = harness.makeNode(), goldilocks = harness.makeNode(); - goldilocks.untrust(); - - // Stagger the validations around sequence 2 - // papa on seq 1 is behind - // baby on seq 2 is just right - // mama on seq 3 is ahead - // goldilocks on seq 2, but is not trusted - - for (auto const& node : {baby, papa, mama, goldilocks}) - 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, Ledger::Seq{2}, Ledger::ID{2})); - - harness.clock().advance(1s); - 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( - Ledger::ID{2}, // Current ledger - Ledger::ID{1}, // Prior ledger - Ledger::Seq{0}); // No cutoff - - BEAST_EXPECT(res.size() == 1); - 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( - Ledger::ID{2}, // Current ledger - Ledger::ID{0}, // No prior ledger - Ledger::Seq{0}); // No cutoff - - BEAST_EXPECT(res.size() == 2); - 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( - 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[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( - 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[Ledger::ID{2}] == 2); - BEAST_EXPECT( - getPreferredLedger(Ledger::ID{2}, res) == Ledger::ID{2}); - } - } - void testTrustedByLedgerFunctions() { // Test the Validations functions that calculate a value by ledger ID using namespace std::chrono_literals; + testcase("By ledger functions"); // Several Validations functions return a set of values associated // with trusted ledgers sharing the same ledger ID. The tests below @@ -635,14 +649,19 @@ class Validations_test : public beast::unit_test::suite // verifying that the Validations member functions all calculate the // proper transformation of the available ledgers. - TestHarness harness; + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node a = harness.makeNode(), b = harness.makeNode(), - c = harness.makeNode(), d = harness.makeNode(); + c = harness.makeNode(), d = harness.makeNode(), + e = harness.makeNode(); + c.untrust(); // Mix of load fees a.setLoadFee(12); b.setLoadFee(1); c.setLoadFee(12); + e.setLoadFee(12); hash_map> trustedValidations; @@ -658,9 +677,11 @@ 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; @@ -672,45 +693,60 @@ 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)); } }; //---------------------------------------------------------------------- + Ledger ledgerA = h["a"]; + Ledger ledgerB = h["b"]; + Ledger ledgerAC = h["ac"]; + // Add a dummy ID to cover unknown ledger identifiers trustedValidations[Ledger::ID{100}] = {}; - // first round a,b,c agree, d differs + // first round a,b,c agree for (auto const& node : {a, b, c}) { - auto const val = node.validation(Ledger::Seq{1}, Ledger::ID{1}); - BEAST_EXPECT(AddOutcome::current == harness.add(node, val)); + auto const val = node.validate(ledgerA); + BEAST_EXPECT(ValStatus::current == harness.add(val)); if (val.trusted()) trustedValidations[val.ledgerID()].emplace_back(val); } + // d diagrees { - auto const val = d.validation(Ledger::Seq{1}, Ledger::ID{11}); - BEAST_EXPECT(AddOutcome::current == harness.add(d, val)); + auto const val = d.validate(ledgerB); + BEAST_EXPECT(ValStatus::current == harness.add(val)); trustedValidations[val.ledgerID()].emplace_back(val); } + // e only issues partials + { + BEAST_EXPECT(ValStatus::current == harness.add(e.partial(ledgerA))); + } harness.clock().advance(5s); - // second round, a,b,c move to ledger 2, d now thinks ledger 1 + // second round, a,b,c move to ledger 2 for (auto const& node : {a, b, c}) { - auto const val = node.validation(Ledger::Seq{2}, Ledger::ID{2}); - BEAST_EXPECT(AddOutcome::current == harness.add(node, val)); + auto const val = node.validate(ledgerAC); + BEAST_EXPECT(ValStatus::current == harness.add(val)); if (val.trusted()) trustedValidations[val.ledgerID()].emplace_back(val); } + // d now thinks ledger 1, but cannot re-issue a previously used seq { - 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); + BEAST_EXPECT(ValStatus::badSeq == harness.add(d.partial(ledgerA))); + } + // e only issues partials + { + BEAST_EXPECT( + ValStatus::current == harness.add(e.partial(ledgerAC))); } compare(); @@ -720,16 +756,18 @@ class Validations_test : public beast::unit_test::suite testExpire() { // Verify expiring clears out validations stored by ledger - - TestHarness harness; + testcase("Expire validations"); + LedgerHistoryHelper h; + TestHarness harness(h.oracle); Node a = harness.makeNode(); - BEAST_EXPECT(AddOutcome::current == - harness.add(a, Ledger::Seq{1}, Ledger::ID{1})); - BEAST_EXPECT(harness.vals().numTrustedForLedger(Ledger::ID{1})); + Ledger ledgerA = h["a"]; + + BEAST_EXPECT(ValStatus::current == harness.add(a.validate(ledgerA))); + BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerA.id())); harness.clock().advance(harness.parms().validationSET_EXPIRES); harness.vals().expire(); - BEAST_EXPECT(!harness.vals().numTrustedForLedger(Ledger::ID{1})); + BEAST_EXPECT(!harness.vals().numTrustedForLedger(ledgerA.id())); } void @@ -737,28 +775,31 @@ class Validations_test : public beast::unit_test::suite { // Test final flush of validations using namespace std::chrono_literals; + testcase("Flush validations"); - TestHarness harness; - + LedgerHistoryHelper h; + TestHarness harness(h.oracle); Node a = harness.makeNode(), b = harness.makeNode(), c = harness.makeNode(); c.untrust(); + Ledger ledgerA = h["a"]; + Ledger ledgerAB = h["ab"]; + hash_map expected; for (auto const& node : {a, b, c}) { - auto const val = node.validation(Ledger::Seq{1}, Ledger::ID{1}); - BEAST_EXPECT(AddOutcome::current == harness.add(node, val)); + auto const val = node.validate(ledgerA); + BEAST_EXPECT(ValStatus::current == harness.add(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(Ledger::Seq{2}, Ledger::ID{2}); - BEAST_EXPECT(AddOutcome::current == harness.add(a, newVal)); + auto newVal = a.validate(ledgerAB); + BEAST_EXPECT(ValStatus::current == harness.add(newVal)); expected.find(a.masterKey())->second = newVal; // Now flush @@ -777,52 +818,242 @@ class Validations_test : public beast::unit_test::suite void testGetPreferredLedger() { - using Distribution = hash_map; + using namespace std::chrono_literals; + testcase("Preferred Ledger"); - { - Ledger::ID const current{1}; - Distribution dist; - BEAST_EXPECT(getPreferredLedger(current, dist) == current); - } + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node a = harness.makeNode(), b = harness.makeNode(), + c = harness.makeNode(), d = harness.makeNode(); + c.untrust(); - { - Ledger::ID const current{1}; - Distribution dist; - dist[Ledger::ID{2}] = 2; - BEAST_EXPECT(getPreferredLedger(current, dist) == Ledger::ID{2}); - } + Ledger ledgerA = h["a"]; + Ledger ledgerB = h["b"]; + Ledger ledgerAC = h["ac"]; + Ledger ledgerACD = h["acd"]; - { - 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}); - } + using Seq = Ledger::Seq; + using ID = Ledger::ID; - { - Ledger::ID const current{1}; - Distribution dist; - dist[Ledger::ID{1}] = 2; - dist[Ledger::ID{2}] = 2; - BEAST_EXPECT(getPreferredLedger(current, dist) == current); - } + auto pref = [](Ledger ledger) { + return std::make_pair(ledger.seq(), ledger.id()); + }; - { - Ledger::ID const current{2}; - Distribution dist; - dist[Ledger::ID{1}] = 2; - dist[Ledger::ID{2}] = 2; - BEAST_EXPECT(getPreferredLedger(current, dist) == current); - } + // Empty (no ledgers) + BEAST_EXPECT( + harness.vals().getPreferred(ledgerA) == pref(genesisLedger)); - { - 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}); - } + // Single ledger + BEAST_EXPECT(ValStatus::current == harness.add(a.validate(ledgerB))); + BEAST_EXPECT(harness.vals().getPreferred(ledgerA) == pref(ledgerB)); + BEAST_EXPECT(harness.vals().getPreferred(ledgerB) == pref(ledgerB)); + + // Minimum valid sequence + BEAST_EXPECT( + harness.vals().getPreferred(ledgerA, Seq{10}) == ledgerA.id()); + + // Untrusted doesn't impact preferred ledger + // (ledgerB has tie-break over ledgerA) + BEAST_EXPECT(ValStatus::current == harness.add(b.validate(ledgerA))); + BEAST_EXPECT(ValStatus::current == harness.add(c.validate(ledgerA))); + BEAST_EXPECT(ledgerB.id() > ledgerA.id()); + BEAST_EXPECT(harness.vals().getPreferred(ledgerA) == pref(ledgerB)); + BEAST_EXPECT(harness.vals().getPreferred(ledgerB) == pref(ledgerB)); + + // Partial does break ties + BEAST_EXPECT(ValStatus::current == harness.add(d.partial(ledgerA))); + BEAST_EXPECT(harness.vals().getPreferred(ledgerA) == pref(ledgerA)); + BEAST_EXPECT(harness.vals().getPreferred(ledgerB) == pref(ledgerA)); + + harness.clock().advance(5s); + + // Parent of preferred-> stick with ledger + for (auto const& node : {a, b, c, d}) + BEAST_EXPECT( + ValStatus::current == harness.add(node.validate(ledgerAC))); + // Parent of preferred stays put + BEAST_EXPECT(harness.vals().getPreferred(ledgerA) == pref(ledgerA)); + // Earlier different chain, switch + BEAST_EXPECT(harness.vals().getPreferred(ledgerB) == pref(ledgerAC)); + // Later on chain, stays where it is + BEAST_EXPECT(harness.vals().getPreferred(ledgerACD) == pref(ledgerACD)); + + // Any later grandchild or different chain is preferred + harness.clock().advance(5s); + for (auto const& node : {a, b, c, d}) + BEAST_EXPECT( + ValStatus::current == harness.add(node.validate(ledgerACD))); + for (auto const& ledger : {ledgerA, ledgerB, ledgerACD}) + BEAST_EXPECT( + harness.vals().getPreferred(ledger) == pref(ledgerACD)); + } + + void + testGetPreferredLCL() + { + using namespace std::chrono_literals; + testcase("Get preferred LCL"); + + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node a = harness.makeNode(); + + Ledger ledgerA = h["a"]; + Ledger ledgerB = h["b"]; + Ledger ledgerC = h["c"]; + + using ID = Ledger::ID; + using Seq = Ledger::Seq; + + hash_map peerCounts; + + // No trusted validations or counts sticks with current ledger + BEAST_EXPECT( + harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == + ledgerA.id()); + + ++peerCounts[ledgerB.id()]; + + // No trusted validations, rely on peer counts + BEAST_EXPECT( + harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == + ledgerB.id()); + + ++peerCounts[ledgerC.id()]; + // No trusted validations, tied peers goes with larger ID + BEAST_EXPECT(ledgerC.id() > ledgerB.id()); + + BEAST_EXPECT( + harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == + ledgerC.id()); + + peerCounts[ledgerC.id()] += 1000; + + // Single trusted always wins over peer counts + BEAST_EXPECT(ValStatus::current == harness.add(a.validate(ledgerA))); + BEAST_EXPECT( + harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == + ledgerA.id()); + BEAST_EXPECT( + harness.vals().getPreferredLCL(ledgerB, Seq{0}, peerCounts) == + ledgerA.id()); + BEAST_EXPECT( + harness.vals().getPreferredLCL(ledgerC, Seq{0}, peerCounts) == + ledgerA.id()); + + // Stick with current ledger if trusted validation ledger has too old + // of a sequence + BEAST_EXPECT( + harness.vals().getPreferredLCL(ledgerB, Seq{2}, peerCounts) == + ledgerB.id()); + } + + void + testAcquireValidatedLedger() + { + using namespace std::chrono_literals; + testcase("Acquire validated ledger"); + + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node a = harness.makeNode(); + Node b = harness.makeNode(); + + using ID = Ledger::ID; + using Seq = Ledger::Seq; + + // Validate the ledger before it is actually available + Validation val = a.validate(ID{2}, Seq{2}, 0s, 0s, true); + + BEAST_EXPECT(ValStatus::current == harness.add(val)); + // Validation is available + BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{2}) == 1); + // but ledger based data is not + BEAST_EXPECT(harness.vals().getNodesAfter(genesisLedger, ID{0}) == 0); + // Initial preferred branch falls back to the ledger we are trying to + // acquire + BEAST_EXPECT( + harness.vals().getPreferred(genesisLedger) == + std::make_pair(Seq{2}, ID{2})); + + // After adding another unavailable validation, the preferred ledger + // breaks ties via higher ID + BEAST_EXPECT( + ValStatus::current == + harness.add(b.validate(ID{3}, Seq{2}, 0s, 0s, true))); + BEAST_EXPECT( + harness.vals().getPreferred(genesisLedger) == + std::make_pair(Seq{2}, ID{3})); + + // Create the ledger + Ledger ledgerAB = h["ab"]; + // Now it should be available + BEAST_EXPECT(harness.vals().getNodesAfter(genesisLedger, ID{0}) == 1); + + // Create a validation that is not available + harness.clock().advance(5s); + Validation val2 = a.validate(ID{4}, Seq{4}, 0s, 0s, true); + BEAST_EXPECT(ValStatus::current == harness.add(val2)); + BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{4}) == 1); + BEAST_EXPECT( + harness.vals().getPreferred(genesisLedger) == + std::make_pair(ledgerAB.seq(), ledgerAB.id())); + + // Another node requesting that ledger still doesn't change things + Validation val3 = b.validate(ID{4}, Seq{4}, 0s, 0s, true); + BEAST_EXPECT(ValStatus::current == harness.add(val3)); + BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{4}) == 2); + BEAST_EXPECT( + harness.vals().getPreferred(genesisLedger) == + std::make_pair(ledgerAB.seq(), ledgerAB.id())); + + // Switch to validation that is available + harness.clock().advance(5s); + Ledger ledgerABCDE = h["abcde"]; + BEAST_EXPECT(ValStatus::current == harness.add(a.partial(ledgerABCDE))); + BEAST_EXPECT(ValStatus::current == harness.add(b.partial(ledgerABCDE))); + BEAST_EXPECT( + harness.vals().getPreferred(genesisLedger) == + std::make_pair(ledgerABCDE.seq(), ledgerABCDE.id())); + } + + void + testNumTrustedForLedger() + { + testcase("NumTrustedForLedger"); + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node a = harness.makeNode(); + Node b = harness.makeNode(); + Ledger ledgerA = h["a"]; + + BEAST_EXPECT(ValStatus::current == harness.add(a.partial(ledgerA))); + BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerA.id()) == 0); + + BEAST_EXPECT(ValStatus::current == harness.add(b.validate(ledgerA))); + BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerA.id()) == 1); + } + + void + testSeqEnforcer() + { + testcase("SeqEnforcer"); + using Seq = Ledger::Seq; + using namespace std::chrono; + + beast::manual_clock clock; + SeqEnforcer enforcer; + + ValidationParms p; + + BEAST_EXPECT(enforcer(clock.now(), Seq{1}, p)); + BEAST_EXPECT(enforcer(clock.now(), Seq{10}, p)); + BEAST_EXPECT(!enforcer(clock.now(), Seq{5}, p)); + BEAST_EXPECT(!enforcer(clock.now(), Seq{9}, p)); + clock.advance(p.validationSET_EXPIRES - 1ms); + BEAST_EXPECT(!enforcer(clock.now(), Seq{1}, p)); + clock.advance(2ms); + BEAST_EXPECT(enforcer(clock.now(), Seq{1}, p)); } void @@ -833,15 +1064,18 @@ class Validations_test : public beast::unit_test::suite testGetNodesAfter(); testCurrentTrusted(); testGetCurrentPublicKeys(); - testCurrentTrustedDistribution(); testTrustedByLedgerFunctions(); testExpire(); testFlush(); testGetPreferredLedger(); + testGetPreferredLCL(); + testAcquireValidatedLedger(); + testNumTrustedForLedger(); + testSeqEnforcer(); } }; BEAST_DEFINE_TESTSUITE(Validations, consensus, ripple); -} // csf -} // test -} // ripple +} // namespace csf +} // namespace test +} // namespace ripple diff --git a/src/test/csf/Peer.h b/src/test/csf/Peer.h index fa07dd3dea4..19c94c82bd3 100644 --- a/src/test/csf/Peer.h +++ b/src/test/csf/Peer.h @@ -110,14 +110,30 @@ struct Peer } }; - /** Generic Validations policy that simply ignores recently stale validations + /** Generic Validations adaptor that simply ignores recently stale validations */ - class StalePolicy + class ValAdaptor { Peer& p_; public: - StalePolicy(Peer& p) : p_{p} + struct Mutex + { + void + lock() + { + } + + void + unlock() + { + } + }; + + using Validation = csf::Validation; + using Ledger = csf::Ledger; + + ValAdaptor(Peer& p) : p_{p} { } @@ -136,20 +152,13 @@ struct Peer flush(hash_map&& remaining) { } - }; - /** Non-locking mutex to avoid locks in generic Validations - */ - struct NotAMutex - { - void - lock() - { - } - - void - unlock() + boost::optional + acquire(Ledger::ID const & id) { + if(Ledger const * ledger = p_.acquireLedger(id)) + return *ledger; + return boost::none; } }; @@ -195,9 +204,7 @@ struct Peer hash_map ledgers; //! Validations from trusted nodes - Validations validations; - using AddOutcome = - Validations::AddOutcome; + Validations validations; //! The most recent ledger that has been fully validated by the network from //! the perspective of this Peer @@ -213,12 +220,13 @@ struct Peer //! 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; + // Ledgers/TxSets we are acquiring and when that request times out + bc::flat_map acquiringLedgers; + bc::flat_map 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(); @@ -231,6 +239,9 @@ struct Peer //! Whether to simulate running as validator or a tracking node bool runAsValidator = true; + //! Enforce invariants on validation sequence numbers + SeqEnforcer seqEnforcer; + //TODO: Consider removing these two, they are only a convenience for tests // Number of proposers in the prior round std::size_t prevProposers = 0; @@ -275,7 +286,9 @@ struct Peer , scheduler{s} , net{n} , trustGraph(tg) - , validations{ValidationParms{}, s.clock(), j, *this} + , lastClosedLedger{Ledger::MakeGenesis{}} + , validations{ValidationParms{}, s.clock(), *this} + , fullyValidatedLedger{Ledger::MakeGenesis{}} , collectors{c} { // All peers start from the default constructed genesis ledger @@ -380,16 +393,30 @@ struct Peer Ledger const* acquireLedger(Ledger::ID const& ledgerID) { + using namespace std::chrono; + auto it = ledgers.find(ledgerID); if (it != ledgers.end()) return &(it->second); - // Don't retry if we already are acquiring it - if(!acquiringLedgers.emplace(ledgerID).second) + // No peers + if(net.links(this).empty()) return nullptr; + // Don't retry if we already are acquiring it and haven't timed out + auto aIt = acquiringLedgers.find(ledgerID); + if(aIt!= acquiringLedgers.end()) + { + if(scheduler.now() < aIt->second) + return nullptr; + } + + + SimDuration minDuration{10s}; for (auto const& link : net.links(this)) { + minDuration = std::min(minDuration, link.data.delay); + // Send a messsage to neighbors to find the ledger net.send( this, link.target, [ to = link.target, from = this, ledgerID ]() { @@ -400,11 +427,13 @@ struct Peer // requesting peer where it is added to the available // ledgers to->net.send(to, from, [ from, ledger = it->second ]() { + from->acquiringLedgers.erase(ledger.id()); from->ledgers.emplace(ledger.id(), ledger); }); } }); } + acquiringLedgers[ledgerID] = scheduler.now() + 2 * minDuration; return nullptr; } @@ -416,12 +445,22 @@ struct Peer if (it != txSets.end()) return &(it->second); - // Don't retry if we already are acquiring it - if(!acquiringTxSets.emplace(setId).second) + // No peers + if(net.links(this).empty()) return nullptr; + // Don't retry if we already are acquiring it and haven't timed out + auto aIt = acquiringTxSets.find(setId); + if(aIt!= acquiringTxSets.end()) + { + if(scheduler.now() < aIt->second) + return nullptr; + } + + SimDuration minDuration{10s}; for (auto const& link : net.links(this)) { + minDuration = std::min(minDuration, link.data.delay); // Send a message to neighbors to find the tx set net.send( this, link.target, [ to = link.target, from = this, setId ]() { @@ -432,11 +471,13 @@ struct Peer // 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->acquiringTxSets.erase(txSet.id()); from->handle(txSet); }); } }); } + acquiringTxSets[setId] = scheduler.now() + 2 * minDuration; return nullptr; } @@ -453,9 +494,9 @@ struct Peer } std::size_t - proposersFinished(Ledger::ID const& prevLedger) + proposersFinished(Ledger const & prevLedger, Ledger::ID const& prevLedgerID) { - return validations.getNodesAfter(prevLedger); + return validations.getNodesAfter(prevLedger, prevLedgerID); } Result @@ -504,7 +545,10 @@ struct Peer ConsensusMode const& mode, Json::Value&& consensusJson) { - schedule(delays.ledgerAccept, [&]() { + schedule(delays.ledgerAccept, [=]() { + const bool proposing = mode == ConsensusMode::proposing; + const bool consensusFail = result.state == ConsensusState::MovedOn; + TxSet const acceptedTxs = injectTxs(prevLedger, result.set); Ledger const newLedger = oracle.accept( prevLedger, @@ -527,18 +571,23 @@ struct Peer // Only send validation if the new ledger is compatible with our // fully validated ledger bool const isCompatible = - oracle.isAncestor(fullyValidatedLedger, newLedger); + newLedger.isAncestor(fullyValidatedLedger); - if (runAsValidator && isCompatible) + // Can only send one validated ledger per seq + if (runAsValidator && isCompatible && !consensusFail && + seqEnforcer( + scheduler.now(), newLedger.seq(), validations.parms())) { + bool isFull = proposing; + Validation v{newLedger.id(), newLedger.seq(), now(), now(), key, id, - false}; - // share is not trusted + isFull}; + // share the new validation; it is trusted by the receiver share(v); // we trust ourselves addTrustedValidation(v); @@ -564,9 +613,7 @@ struct Peer Ledger::Seq earliestAllowedSeq() const { - if (lastClosedLedger.seq() > Ledger::Seq{20}) - return lastClosedLedger.seq() - Ledger::Seq{20}; - return Ledger::Seq{0}; + return fullyValidatedLedger.seq(); } Ledger::ID @@ -579,22 +626,15 @@ struct Peer 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); + Ledger::ID const netLgr = + validations.getPreferred(ledger, earliestAllowedSeq()); if (netLgr != ledgerID) { + JLOG(j.trace()) << Json::Compact(validations.getJsonTrie()); issue(WrongPrevLedger{ledgerID, netLgr}); } + return netLgr; } @@ -641,9 +681,9 @@ struct Peer { v.setTrusted(); v.setSeen(now()); - AddOutcome const res = validations.add(v.key(), v); + ValStatus const res = validations.add(v.key(), v); - if(res == AddOutcome::stale || res == AddOutcome::repeat) + if(res == ValStatus::stale || res == ValStatus::repeatID) return false; // Acquire will try to get from network if not already local @@ -663,7 +703,7 @@ struct Peer 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) + if (count >= quorum && ledger.isAncestor(fullyValidatedLedger)) { issue(FullyValidateLedger{ledger, fullyValidatedLedger}); fullyValidatedLedger = ledger; @@ -825,21 +865,16 @@ struct Peer 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); + // Between rounds, we take the majority ledger + // In the future, consider taking peer dominant ledger if no validations + // yet + Ledger::ID bestLCL = + validations.getPreferred(lastClosedLedger, earliestAllowedSeq()); + if(bestLCL == Ledger::ID{0}) + bestLCL = lastClosedLedger.id(); 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); } diff --git a/src/test/csf/PeerGroup.h b/src/test/csf/PeerGroup.h index 66ff962577a..4ad8bd5396e 100644 --- a/src/test/csf/PeerGroup.h +++ b/src/test/csf/PeerGroup.h @@ -47,7 +47,6 @@ class PeerGroup using const_reference = peers_type::const_reference; PeerGroup() = default; - PeerGroup(PeerGroup const&) = default; PeerGroup(Peer* peer) : peers_{1, peer} { } @@ -62,7 +61,6 @@ class PeerGroup PeerGroup(std::set const& peers) : peers_{peers.begin(), peers.end()} { - } iterator @@ -101,6 +99,14 @@ class PeerGroup return std::find(peers_.begin(), peers_.end(), p) != peers_.end(); } + bool + contains(PeerID id) + { + return std::find_if(peers_.begin(), peers_.end(), [id](Peer const* p) { + return p->id == id; + }) != peers_.end(); + } + std::size_t size() const { diff --git a/src/test/csf/Sim.h b/src/test/csf/Sim.h index 23c10d735fd..02295d750ea 100644 --- a/src/test/csf/Sim.h +++ b/src/test/csf/Sim.h @@ -65,6 +65,7 @@ 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; + PeerGroup allPeers; public: std::mt19937_64 rng; @@ -113,7 +114,9 @@ class Sim j); newPeers.emplace_back(&peers.back()); } - return PeerGroup{newPeers}; + PeerGroup res{newPeers}; + allPeers = allPeers + res; + return res; } //! The number of peers in the simulation @@ -136,18 +139,28 @@ class Sim void run(SimDuration const& dur); - /** Check whether all peers in the network are synchronized. + /** Check whether all peers in the group are synchronized. - Nodes in the network are synchronized if they share the same last + Nodes in the group are synchronized if they share the same last fully validated and last generated ledger. */ bool + synchronized(PeerGroup const& g) const; + + /** Check whether all peers in the network are synchronized + */ + bool synchronized() const; - /** Calculate the number of branches in the network. + /** Calculate the number of branches in the group. + + A branch occurs if two nodes in the group have fullyValidatedLedgers + that are not on the same chain of ledgers. + */ + std::size_t + branches(PeerGroup const& g) const; - A branch occurs if two peers have fullyValidatedLedgers that are not on - the same chain of ledgers. + /** Calculate the number of branches in the network */ std::size_t branches() const; diff --git a/src/test/csf/Validation.h b/src/test/csf/Validation.h index ab45e4878c3..80b74e4b844 100644 --- a/src/test/csf/Validation.h +++ b/src/test/csf/Validation.h @@ -53,17 +53,21 @@ class Validation NetClock::time_point seenTime_; PeerKey key_; PeerID nodeID_{0}; - bool trusted_ = true; + bool trusted_ = false; + bool full_ = false; boost::optional loadFee_; public: + using NodeKey = PeerKey; + using NodeID = PeerID; + Validation(Ledger::ID id, Ledger::Seq seq, NetClock::time_point sign, NetClock::time_point seen, PeerKey key, PeerID nodeID, - bool trusted, + bool full, boost::optional loadFee = boost::none) : ledgerID_{id} , seq_{seq} @@ -71,7 +75,7 @@ class Validation , seenTime_{seen} , key_{key} , nodeID_{nodeID} - , trusted_{trusted} + , full_{full} , loadFee_{loadFee} { } @@ -118,6 +122,13 @@ class Validation return trusted_; } + bool + full() const + { + return full_; + } + + boost::optional loadFee() const { @@ -133,8 +144,16 @@ class Validation auto asTie() const { - return std::tie(ledgerID_, seq_, signTime_, seenTime_, key_, nodeID_, - trusted_, loadFee_); + // trusted is a status set by the receiver, so it is not part of the tie + return std::tie( + ledgerID_, + seq_, + signTime_, + seenTime_, + key_, + nodeID_, + loadFee_, + full_); } bool operator==(Validation const& o) const diff --git a/src/test/csf/impl/Sim.cpp b/src/test/csf/impl/Sim.cpp index 2860744ca62..bb3bb1a789a 100644 --- a/src/test/csf/impl/Sim.cpp +++ b/src/test/csf/impl/Sim.cpp @@ -48,25 +48,36 @@ Sim::run(SimDuration const & dur) bool Sim::synchronized() const { - if (peers.size() < 1) + return synchronized(allPeers); +} + +bool +Sim::synchronized(PeerGroup const & g) const +{ + if (g.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(); + Peer const * ref = g[0]; + return std::all_of(g.begin(), g.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 branches(allPeers); +} +std::size_t +Sim::branches(PeerGroup const & g) const +{ + if(g.size() < 1) return 0; std::set ledgers; - for(auto const & peer : peers) - ledgers.insert(peer.fullyValidatedLedger); + for(auto const & peer : g) + ledgers.insert(peer->fullyValidatedLedger); return oracle.branches(ledgers); } diff --git a/src/test/csf/impl/ledgers.cpp b/src/test/csf/impl/ledgers.cpp index eb8e9a7adab..1fffb902574 100644 --- a/src/test/csf/impl/ledgers.cpp +++ b/src/test/csf/impl/ledgers.cpp @@ -18,6 +18,7 @@ //============================================================================== #include #include +#include #include @@ -36,6 +37,53 @@ Ledger::getJson() const return res; } +bool +Ledger::isAncestor(Ledger const& ancestor) const +{ + if (ancestor.seq() < seq()) + return operator[](ancestor.seq()) == ancestor.id(); + return false; +} + +Ledger::ID +Ledger::operator[](Seq s) const +{ + if(s > seq()) + return {}; + if(s== seq()) + return id(); + return instance_->ancestors[static_cast(s)]; + +} + +Ledger::Seq +mismatch(Ledger const& a, Ledger const& b) +{ + using Seq = Ledger::Seq; + + // end is 1 past end of range + Seq start{0}; + Seq end = std::min(a.seq() + Seq{1}, b.seq() + Seq{1}); + + // Find mismatch in [start,end) + // Binary search + Seq count = end - start; + while(count > Seq{0}) + { + Seq step = count/Seq{2}; + Seq curr = start + step; + if(a[curr] == b[curr]) + { + // go to second half + start = ++curr; + count -= step + Seq{1}; + } + else + count = step; + } + return start; +} + LedgerOracle::LedgerOracle() { instances_.insert(InstanceEntry{Ledger::genesis, nextID()}); @@ -67,6 +115,8 @@ LedgerOracle::accept( next.parentCloseTime = parent.closeTime(); next.parentID = parent.id(); + next.ancestors.push_back(parent.id()); + auto it = instances_.left.find(next); if (it == instances_.left.end()) { @@ -88,19 +138,6 @@ LedgerOracle::lookup(Ledger::ID const & id) const } -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 { @@ -121,7 +158,7 @@ LedgerOracle::branches(std::set const & ledgers) const 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)) + if (later.isAncestor(earlier)) { tips[idx] = later; found = true; diff --git a/src/test/csf/ledgers.h b/src/test/csf/ledgers.h index 9e48d933144..8ae8a38584e 100644 --- a/src/test/csf/ledgers.h +++ b/src/test/csf/ledgers.h @@ -66,6 +66,7 @@ class Ledger struct IdTag; using ID = tagged_integer; + struct MakeGenesis {}; private: // The instance is the common immutable data that will be assigned a unique // ID by the oracle @@ -94,6 +95,11 @@ class Ledger //! Parent ledger close time NetClock::time_point parentCloseTime; + //! IDs of this ledgers ancestors. Since each ledger already has unique + //! ancestors based on the parentID, this member is not needed for any + //! of the operators below. + std::vector ancestors; + auto asTie() const { @@ -137,7 +143,13 @@ class Ledger } public: - Ledger() : id_{0}, instance_(&genesis) + Ledger(MakeGenesis) : instance_(&genesis) + { + } + + // This is required by the generic Consensus for now and should be + // migrated to the MakeGenesis approach above. + Ledger() : Ledger(MakeGenesis{}) { } @@ -189,6 +201,21 @@ class Ledger return instance_->txs; } + /** Determine whether ancestor is really an ancestor of this ledger */ + bool + isAncestor(Ledger const& ancestor) const; + + /** Return the id of the ancestor with the given seq (if exists/known) + */ + ID + operator[](Seq seq) const; + + /** Return the sequence number of the first mismatching ancestor + */ + friend + Ledger::Seq + mismatch(Ledger const & a, Ledger const & o); + Json::Value getJson() const; friend bool @@ -238,9 +265,16 @@ class LedgerOracle 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; + Ledger + accept(Ledger const& curr, Tx tx) + { + using namespace std::chrono_literals; + return accept( + curr, + TxSetType{tx}, + curr.closeTimeResolution(), + curr.closeTime() + 1s); + } /** Determine the number of distinct branches for the set of ledgers. @@ -256,6 +290,57 @@ class LedgerOracle }; +/** Helper for writing unit tests with controlled ledger histories. + + This class allows clients to refer to distinct ledgers as strings, where + each character in the string indicates a unique ledger. It enforces the + uniqueness at runtime, but this simplifies creation of alternate ledger + histories, e.g. + + HistoryHelper hh; + hh["a"] + hh["ab"] + hh["ac"] + hh["abd"] + + Creates a history like + b - d + / + a - c + +*/ +struct LedgerHistoryHelper +{ + LedgerOracle oracle; + Tx::ID nextTx{0}; + std::unordered_map ledgers; + std::set seen; + + LedgerHistoryHelper() + { + ledgers[""] = Ledger{Ledger::MakeGenesis{}}; + } + + /** Get or create the ledger with the given string history. + + Creates any necessary intermediate ledgers, but asserts if + a letter is re-used (e.g. "abc" then "adc" would assert) + */ + Ledger const& operator[](std::string const& s) + { + auto it = ledgers.find(s); + if (it != ledgers.end()) + return it->second; + + // enforce that the new suffix has never been seen + assert(seen.emplace(s.back()).second); + + Ledger const& parent = (*this)[s.substr(0, s.size() - 1)]; + return ledgers.emplace(s, oracle.accept(parent, ++nextTx)) + .first->second; + } +}; + } // csf } // test } // ripple diff --git a/src/test/unity/app_test_unity2.cpp b/src/test/unity/app_test_unity2.cpp index 2253e00fd5e..e9ea03e1d42 100644 --- a/src/test/unity/app_test_unity2.cpp +++ b/src/test/unity/app_test_unity2.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/unity/consensus_test_unity.cpp b/src/test/unity/consensus_test_unity.cpp index 1a9a347a336..f73a01d919e 100644 --- a/src/test/unity/consensus_test_unity.cpp +++ b/src/test/unity/consensus_test_unity.cpp @@ -21,5 +21,6 @@ #include #include #include +#include #include #include