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