mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-29 18:10:34 +00:00
1180 lines
38 KiB
C++
1180 lines
38 KiB
C++
#pragma once
|
|
|
|
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/basics/UnorderedContainers.h>
|
|
#include <xrpl/basics/chrono.h>
|
|
#include <xrpl/beast/clock/abstract_clock.h>
|
|
#include <xrpl/beast/container/aged_container_utility.h>
|
|
#include <xrpl/beast/container/aged_unordered_map.h>
|
|
#include <xrpl/beast/hash/uhash.h>
|
|
#include <xrpl/beast/utility/Journal.h>
|
|
#include <xrpl/beast/utility/instrumentation.h>
|
|
#include <xrpl/consensus/LedgerTrie.h>
|
|
#include <xrpl/json/json_value.h>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace xrpl {
|
|
|
|
/**
|
|
* Timing parameters to control validation staleness and expiration.
|
|
*
|
|
* @note These are protocol level parameters that should not be changed without
|
|
* careful consideration. They are *not* implemented as static constexpr
|
|
* to allow simulation code to test alternate parameter settings.
|
|
*/
|
|
struct ValidationParms
|
|
{
|
|
explicit ValidationParms() = default;
|
|
|
|
/**
|
|
* The number of seconds a validation remains current after its ledger's
|
|
* close time.
|
|
*
|
|
* This is a safety to protect against very old validations and the time
|
|
* it takes to adjust the close time accuracy window.
|
|
*/
|
|
std::chrono::seconds validationCurrentWall = std::chrono::minutes{5};
|
|
|
|
/**
|
|
* Duration a validation remains current after first observed.
|
|
*
|
|
* The number of seconds a validation remains current after the time we
|
|
* first saw it. This provides faster recovery in very rare cases where the
|
|
* number of validations produced by the network is lower than normal
|
|
*/
|
|
std::chrono::seconds validationCurrentLocal = std::chrono::minutes{3};
|
|
|
|
/**
|
|
* Duration pre-close in which validations are acceptable.
|
|
*
|
|
* The number of seconds before a close time that we consider a validation
|
|
* acceptable. This protects against extreme clock errors
|
|
*/
|
|
std::chrono::seconds validationCurrentEarly = std::chrono::minutes{3};
|
|
|
|
/**
|
|
* Duration a set of validations for a given ledger hash remain valid
|
|
*
|
|
* The number of seconds before a set of validations for a given ledger
|
|
* hash can expire. This keeps validations for recent ledgers available
|
|
* for a reasonable interval.
|
|
*/
|
|
std::chrono::seconds validationSetExpires = std::chrono::minutes{10};
|
|
|
|
/**
|
|
* How long we consider a validation fresh.
|
|
*
|
|
* The number of seconds since a validation has been seen for it to
|
|
* be considered to accurately represent a live proposer's most recent
|
|
* validation. This value should be sufficiently higher than
|
|
* ledgerMAX_CONSENSUS such that validators who are waiting for
|
|
* laggards are not considered offline.
|
|
*/
|
|
std::chrono::seconds validationFRESHNESS = std::chrono::seconds{20};
|
|
};
|
|
|
|
/**
|
|
* 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 Seq>
|
|
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.validationSetExpires))
|
|
seq_ = Seq{0};
|
|
if (s <= seq_)
|
|
return false;
|
|
seq_ = s;
|
|
when_ = now;
|
|
return true;
|
|
}
|
|
|
|
[[nodiscard]] Seq
|
|
largest() const
|
|
{
|
|
return seq_;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Whether a validation is still current
|
|
*
|
|
* Determines whether a validation can still be considered the current
|
|
* validation from a node based on when it was signed by that node and first
|
|
* seen by this node.
|
|
*
|
|
* @param p ValidationParms with timing parameters
|
|
* @param now Current time
|
|
* @param signTime When the validation was signed
|
|
* @param seenTime When the validation was first seen locally
|
|
*/
|
|
inline bool
|
|
isCurrent(
|
|
ValidationParms const& p,
|
|
NetClock::time_point now,
|
|
NetClock::time_point signTime,
|
|
NetClock::time_point seenTime)
|
|
{
|
|
// Because this can be called on untrusted, possibly
|
|
// malicious validations, we do our math in a way
|
|
// that avoids any chance of overflowing or underflowing
|
|
// the signing time. All of the expressions below are
|
|
// promoted from unsigned 32 bit to signed 64 bit prior
|
|
// to computation.
|
|
|
|
return (signTime > (now - p.validationCurrentEarly)) &&
|
|
(signTime < (now + p.validationCurrentWall)) &&
|
|
((seenTime == NetClock::time_point{}) || (seenTime < (now + p.validationCurrentLocal)));
|
|
}
|
|
|
|
/**
|
|
* Status of validation we received
|
|
*/
|
|
enum class ValStatus {
|
|
/**
|
|
* This was a new validation and was added
|
|
*/
|
|
Current,
|
|
/**
|
|
* Not current or was older than current from this node
|
|
*/
|
|
Stale,
|
|
/**
|
|
* A validation violates the increasing seq requirement
|
|
*/
|
|
BadSeq,
|
|
/**
|
|
* Multiple validations by a validator for the same ledger
|
|
*/
|
|
Multiple,
|
|
/**
|
|
* Multiple validations by a validator for different ledgers
|
|
*/
|
|
Conflicting
|
|
};
|
|
|
|
inline std::string
|
|
to_string(ValStatus m)
|
|
{
|
|
switch (m)
|
|
{
|
|
case ValStatus::Current:
|
|
return "current";
|
|
case ValStatus::Stale:
|
|
return "stale";
|
|
case ValStatus::BadSeq:
|
|
return "badSeq";
|
|
case ValStatus::Multiple:
|
|
return "multiple";
|
|
case ValStatus::Conflicting:
|
|
return "conflicting";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maintains current and recent ledger validations.
|
|
*
|
|
* Manages storage and queries related to validations received on the network.
|
|
* Stores the most current validation from nodes and sets of recent
|
|
* validations grouped by ledger identifier.
|
|
*
|
|
* Stored validations are not necessarily from trusted nodes, so clients
|
|
* and implementations should take care to use `trusted` member functions or
|
|
* check the validation's trusted status.
|
|
*
|
|
* 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 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
|
|
*
|
|
* // Conforms to the Ledger type requirements of LedgerTrie
|
|
* struct Ledger;
|
|
*
|
|
* struct Validation
|
|
* {
|
|
* using NodeID = ...;
|
|
* using NodeKey = ...;
|
|
*
|
|
* // Ledger ID associated with this validation
|
|
* Ledger::ID ledgerID() const;
|
|
*
|
|
* // Sequence number of validation's ledger (0 means no sequence number)
|
|
* Ledger::Seq seq() const
|
|
*
|
|
* // When the validation was signed
|
|
* NetClock::time_point signTime() const;
|
|
*
|
|
* // When the validation was first observed by this node
|
|
* NetClock::time_point seenTime() const;
|
|
*
|
|
* // Signing key of node that published the validation
|
|
* NodeKey key() const;
|
|
*
|
|
* // Whether the publishing node was trusted at the time the validation
|
|
* // arrived
|
|
* bool trusted() const;
|
|
*
|
|
* // Set the validation as trusted
|
|
* void setTrusted();
|
|
*
|
|
* // Set the validation as untrusted
|
|
* void setUntrusted();
|
|
*
|
|
* // Whether this is a full or partial validation
|
|
* bool full() const;
|
|
*
|
|
* // Identifier for this node that remains fixed even when rotating
|
|
* // signing keys
|
|
* NodeID nodeID() const;
|
|
*
|
|
* implementation_specific_t
|
|
* unwrap() -> return the implementation-specific type being wrapped
|
|
*
|
|
* // ... implementation specific
|
|
* };
|
|
*
|
|
* class Adaptor
|
|
* {
|
|
* using Mutex = std::mutex;
|
|
* using Validation = Validation;
|
|
* using Ledger = Ledger;
|
|
*
|
|
* // Return the current network time (used to determine staleness)
|
|
* NetClock::time_point now() const;
|
|
*
|
|
* // Attempt to acquire a specific ledger.
|
|
* std::optional<Ledger> acquire(Ledger::ID const & ledgerID);
|
|
*
|
|
* // ... implementation specific
|
|
* };
|
|
* @endcode
|
|
*
|
|
* @tparam Adaptor Provides type definitions and callbacks
|
|
*/
|
|
template <class Adaptor>
|
|
class Validations
|
|
{
|
|
using Mutex = Adaptor::Mutex;
|
|
using Validation = Adaptor::Validation;
|
|
using Ledger = Adaptor::Ledger;
|
|
using ID = Ledger::ID;
|
|
using Seq = Ledger::Seq;
|
|
using NodeID = Validation::NodeID;
|
|
using NodeKey = Validation::NodeKey;
|
|
|
|
using WrappedValidationType =
|
|
std::decay_t<std::invoke_result_t<decltype(&Validation::unwrap), Validation>>;
|
|
|
|
// Manages concurrent access to members
|
|
mutable Mutex mutex_;
|
|
|
|
// Validations from currently listed and trusted nodes (partial and full)
|
|
hash_map<NodeID, Validation> current_;
|
|
|
|
// Used to enforce the largest validation invariant for the local node
|
|
SeqEnforcer<Seq> localSeqEnforcer_;
|
|
|
|
// Sequence of the largest validation received from each node
|
|
hash_map<NodeID, SeqEnforcer<Seq>> seqEnforcers_;
|
|
|
|
/**
|
|
* Validations from listed nodes, indexed by ledger id (partial and full)
|
|
*/
|
|
beast::aged_unordered_map<
|
|
ID,
|
|
hash_map<NodeID, Validation>,
|
|
std::chrono::steady_clock,
|
|
beast::Uhash<>>
|
|
byLedger_;
|
|
|
|
// Partial and full validations indexed by sequence
|
|
beast::aged_unordered_map<
|
|
Seq,
|
|
hash_map<NodeID, Validation>,
|
|
std::chrono::steady_clock,
|
|
beast::Uhash<>>
|
|
bySequence_;
|
|
|
|
// A range [low_, high_) of validations to keep from expire
|
|
struct KeepRange
|
|
{
|
|
Seq low;
|
|
Seq high;
|
|
};
|
|
std::optional<KeepRange> toKeep_;
|
|
|
|
// Represents the ancestry of validated ledgers
|
|
LedgerTrie<Ledger> trie_;
|
|
|
|
// Last (validated) ledger successfully acquired. If in this map, it is
|
|
// accounted for in the trie.
|
|
hash_map<NodeID, Ledger> lastLedger_;
|
|
|
|
// Set of ledgers being acquired from the network
|
|
hash_map<std::pair<Seq, ID>, hash_set<NodeID>> acquiring_;
|
|
|
|
// 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(std::scoped_lock<Mutex> const&, NodeID const& nodeID, Validation const& val)
|
|
{
|
|
{
|
|
auto it = acquiring_.find(std::make_pair(val.seq(), val.ledgerID()));
|
|
if (it != acquiring_.end())
|
|
{
|
|
it->second.erase(nodeID);
|
|
if (it->second.empty())
|
|
acquiring_.erase(it);
|
|
}
|
|
}
|
|
{
|
|
auto it = lastLedger_.find(nodeID);
|
|
if (it != lastLedger_.end() && it->second.id() == val.ledgerID())
|
|
{
|
|
trie_.remove(it->second);
|
|
lastLedger_.erase(nodeID);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if any pending acquire ledger requests are complete
|
|
void
|
|
checkAcquired(std::scoped_lock<Mutex> const& lock)
|
|
{
|
|
for (auto it = acquiring_.begin(); it != acquiring_.end();)
|
|
{
|
|
if (std::optional<Ledger> ledger = adaptor_.acquire(it->first.second))
|
|
{
|
|
for (NodeID const& nodeID : it->second)
|
|
updateTrie(lock, nodeID, *ledger);
|
|
|
|
it = acquiring_.erase(it);
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the trie to reflect a new validated ledger
|
|
void
|
|
updateTrie(std::scoped_lock<Mutex> const&, NodeID const& nodeID, Ledger ledger)
|
|
{
|
|
auto const [it, inserted] = lastLedger_.emplace(nodeID, ledger);
|
|
if (!inserted)
|
|
{
|
|
trie_.remove(it->second);
|
|
it->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 nodeID The node identifier of 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(
|
|
std::scoped_lock<Mutex> const& lock,
|
|
NodeID const& nodeID,
|
|
Validation const& val,
|
|
std::optional<std::pair<Seq, ID>> prior)
|
|
{
|
|
XRPL_ASSERT(val.trusted(), "xrpl::Validations::updateTrie : trusted input validation");
|
|
|
|
// Clear any prior acquiring ledger for this node
|
|
if (prior)
|
|
{
|
|
auto it = acquiring_.find(*prior);
|
|
if (it != acquiring_.end())
|
|
{
|
|
it->second.erase(nodeID);
|
|
if (it->second.empty())
|
|
acquiring_.erase(it);
|
|
}
|
|
}
|
|
|
|
checkAcquired(lock);
|
|
|
|
std::pair<Seq, ID> const valPair{val.seq(), val.ledgerID()};
|
|
auto it = acquiring_.find(valPair);
|
|
if (it != acquiring_.end())
|
|
{
|
|
it->second.insert(nodeID);
|
|
}
|
|
else
|
|
{
|
|
if (std::optional<Ledger> ledger = adaptor_.acquire(val.ledgerID()))
|
|
{
|
|
updateTrie(lock, nodeID, *ledger);
|
|
}
|
|
else
|
|
{
|
|
acquiring_[valPair].insert(nodeID);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 Invocable with signature (LedgerTrie<Ledger> &)
|
|
*
|
|
* @warning The invocable `f` is expected to be a simple transformation of
|
|
* its arguments and will be called with mutex_ under lock.
|
|
*/
|
|
template <class F>
|
|
auto
|
|
withTrie(std::scoped_lock<Mutex> 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, flushing any which are stale.
|
|
*
|
|
* @param lock Existing lock of mutex_
|
|
* @param pre Invocable with signature (std::size_t) called prior to
|
|
* looping.
|
|
* @param f Invocable with signature (NodeID const &, Validations const &)
|
|
* for each current validation.
|
|
*
|
|
* @note The invocable `pre` is called _prior_ to checking for staleness
|
|
* and reflects an upper-bound on the number of calls to `f.
|
|
* @warning The invocable `f` is expected to be a simple transformation of
|
|
* its arguments and will be called with mutex_ under lock.
|
|
*/
|
|
|
|
template <class Pre, class F>
|
|
void
|
|
current(std::scoped_lock<Mutex> const& lock, Pre&& pre, F&& f)
|
|
{
|
|
NetClock::time_point const t = adaptor_.now();
|
|
pre(current_.size());
|
|
auto it = current_.begin();
|
|
while (it != current_.end())
|
|
{
|
|
// Check for staleness
|
|
if (!isCurrent(parms_, t, it->second.signTime(), it->second.seenTime()))
|
|
{
|
|
removeTrie(lock, it->first, it->second);
|
|
it = current_.erase(it);
|
|
}
|
|
else
|
|
{
|
|
auto cit = typename decltype(current_)::const_iterator{it};
|
|
// contains a live record
|
|
f(cit->first, cit->second);
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 Invocable with signature(std::size_t)
|
|
* @param f Invocable with signature (NodeID const &, Validation const &)
|
|
*
|
|
* @note The invocable `pre` is called prior to iterating validations. The
|
|
* argument is the number of times `f` will be called.
|
|
* @warning The invocable f is expected to be a simple transformation of
|
|
* its arguments and will be called with mutex_ under lock.
|
|
*/
|
|
template <class Pre, class F>
|
|
void
|
|
byLedger(std::scoped_lock<Mutex> const&, ID const& ledgerID, Pre&& pre, F&& f)
|
|
{
|
|
auto it = byLedger_.find(ledgerID);
|
|
if (it != byLedger_.end())
|
|
{
|
|
// Update set time since it is being used
|
|
byLedger_.touch(it);
|
|
pre(it->second.size());
|
|
for (auto const& [key, val] : it->second)
|
|
f(key, val);
|
|
}
|
|
}
|
|
|
|
public:
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param p ValidationParms to control staleness/expiration of validations
|
|
* @param c Clock to use for expiring validations stored by ledger
|
|
* @param ts Parameters for constructing Adaptor instance
|
|
*/
|
|
template <class... Ts>
|
|
Validations(
|
|
ValidationParms const& p,
|
|
beast::AbstractClock<std::chrono::steady_clock>& c,
|
|
Ts&&... ts)
|
|
: byLedger_(c), bySequence_(c), parms_(p), adaptor_(std::forward<Ts>(ts)...)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Return the adaptor instance
|
|
*/
|
|
Adaptor const&
|
|
adaptor() const
|
|
{
|
|
return adaptor_;
|
|
}
|
|
|
|
/**
|
|
* Return the validation timing parameters
|
|
*/
|
|
ValidationParms const&
|
|
parms() const
|
|
{
|
|
return parms_;
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
return localSeqEnforcer_(byLedger_.clock().now(), s, parms_);
|
|
}
|
|
|
|
/**
|
|
* Add a new validation
|
|
*
|
|
* Attempt to add a new validation.
|
|
*
|
|
* @param nodeID The identity of the node issuing this validation
|
|
* @param val The validation to store
|
|
* @return The outcome
|
|
*/
|
|
ValStatus
|
|
add(NodeID const& nodeID, Validation const& val)
|
|
{
|
|
if (!isCurrent(parms_, adaptor_.now(), val.signTime(), val.seenTime()))
|
|
return ValStatus::Stale;
|
|
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
|
|
// Check that validation sequence is greater than any non-expired
|
|
// validations sequence from that validator; if it's not, perform
|
|
// additional work to detect Byzantine validations
|
|
auto const now = byLedger_.clock().now();
|
|
|
|
auto const [seqit, seqinserted] = bySequence_[val.seq()].emplace(nodeID, val);
|
|
|
|
if (!seqinserted)
|
|
{
|
|
// Check if the entry we're already tracking was signed
|
|
// long enough ago that we can disregard it.
|
|
auto const diff = std::max(seqit->second.signTime(), val.signTime()) -
|
|
std::min(seqit->second.signTime(), val.signTime());
|
|
|
|
if (diff > parms_.validationCurrentWall &&
|
|
val.signTime() > seqit->second.signTime())
|
|
seqit->second = val;
|
|
}
|
|
|
|
// Enforce monotonically increasing sequences for validations
|
|
// by a given node, and run the active Byzantine detector:
|
|
if (auto& enf = seqEnforcers_[nodeID]; !enf(now, val.seq(), parms_))
|
|
{
|
|
// If the validation is for the same sequence as one we are
|
|
// tracking, check it closely:
|
|
if (seqit->second.seq() == val.seq())
|
|
{
|
|
// Two validations for the same sequence but for different
|
|
// ledgers. This could be the result of misconfiguration
|
|
// but it can also mean a Byzantine validator.
|
|
if (seqit->second.ledgerID() != val.ledgerID())
|
|
return ValStatus::Conflicting;
|
|
|
|
// Two validations for the same sequence and for the same
|
|
// ledger with different sign times. This could be the
|
|
// result of a misconfiguration but it can also mean a
|
|
// Byzantine validator.
|
|
if (seqit->second.signTime() != val.signTime())
|
|
return ValStatus::Conflicting;
|
|
|
|
// Two validations for the same sequence but with different
|
|
// cookies. This is probably accidental misconfiguration.
|
|
if (seqit->second.cookie() != val.cookie())
|
|
return ValStatus::Multiple;
|
|
}
|
|
|
|
return ValStatus::BadSeq;
|
|
}
|
|
|
|
byLedger_[val.ledgerID()].insert_or_assign(nodeID, val);
|
|
|
|
auto const [it, inserted] = current_.emplace(nodeID, val);
|
|
if (!inserted)
|
|
{
|
|
// Replace existing only if this one is newer
|
|
Validation const& oldVal = it->second;
|
|
if (val.signTime() > oldVal.signTime())
|
|
{
|
|
std::pair<Seq, ID> old(oldVal.seq(), oldVal.ledgerID());
|
|
it->second = val;
|
|
if (val.trusted())
|
|
updateTrie(lock, nodeID, val, old);
|
|
}
|
|
else
|
|
{
|
|
return ValStatus::Stale;
|
|
}
|
|
}
|
|
else if (val.trusted())
|
|
{
|
|
updateTrie(lock, nodeID, val, std::nullopt);
|
|
}
|
|
}
|
|
|
|
return ValStatus::Current;
|
|
}
|
|
|
|
/**
|
|
* Set the range [low, high) of validations to keep from expire
|
|
* @param low the lower sequence number
|
|
* @param high the higher sequence number
|
|
* @note high must be greater than low
|
|
*/
|
|
void
|
|
setSeqToKeep(Seq const& low, Seq const& high)
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
XRPL_ASSERT(low < high, "xrpl::Validations::setSeqToKeep : valid inputs");
|
|
toKeep_ = {low, high};
|
|
}
|
|
|
|
/**
|
|
* Expire old validation sets
|
|
*
|
|
* Remove validation sets that were accessed more than
|
|
* validationSET_EXPIRES ago and were not asked to keep.
|
|
*/
|
|
void
|
|
expire(beast::Journal const& j)
|
|
{
|
|
auto const start = std::chrono::steady_clock::now();
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
if (toKeep_)
|
|
{
|
|
// We only need to refresh the keep range when it's just about
|
|
// to expire. Track the next time we need to refresh.
|
|
static std::chrono::steady_clock::time_point kRefreshTime;
|
|
if (auto const now = byLedger_.clock().now(); kRefreshTime <= now)
|
|
{
|
|
// The next refresh time is shortly before the expiration
|
|
// time from now.
|
|
kRefreshTime = now + parms_.validationSetExpires - parms_.validationFRESHNESS;
|
|
|
|
for (auto i = byLedger_.begin(); i != byLedger_.end(); ++i)
|
|
{
|
|
auto const& validationMap = i->second;
|
|
if (!validationMap.empty())
|
|
{
|
|
auto const seq = validationMap.begin()->second.seq();
|
|
if (toKeep_->low <= seq && seq < toKeep_->high)
|
|
{
|
|
byLedger_.touch(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto i = bySequence_.begin(); i != bySequence_.end(); ++i)
|
|
{
|
|
if (toKeep_->low <= i->first && i->first < toKeep_->high)
|
|
{
|
|
bySequence_.touch(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
beast::expire(byLedger_, parms_.validationSetExpires);
|
|
beast::expire(bySequence_, parms_.validationSetExpires);
|
|
}
|
|
JLOG(j.debug()) << "Validations sets sweep lock duration "
|
|
<< std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - start)
|
|
.count()
|
|
<< "ms";
|
|
}
|
|
|
|
/**
|
|
* Update trust status of validations
|
|
*
|
|
* Updates the trusted status of known validations to account for nodes
|
|
* that have been added or removed from the UNL. This also updates the trie
|
|
* to ensure only currently trusted nodes' validations are used.
|
|
*
|
|
* @param added Identifiers of nodes that are now trusted
|
|
* @param removed Identifiers of nodes that are no longer trusted
|
|
*/
|
|
void
|
|
trustChanged(hash_set<NodeID> const& added, hash_set<NodeID> const& removed)
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
|
|
for (auto& [nodeId, validation] : current_)
|
|
{
|
|
if (added.find(nodeId) != added.end())
|
|
{
|
|
validation.setTrusted();
|
|
updateTrie(lock, nodeId, validation, std::nullopt);
|
|
}
|
|
else if (removed.find(nodeId) != removed.end())
|
|
{
|
|
validation.setUntrusted();
|
|
removeTrie(lock, nodeId, validation);
|
|
}
|
|
}
|
|
|
|
for (auto& [_, validationMap] : byLedger_)
|
|
{
|
|
(void)_;
|
|
for (auto& [nodeId, validation] : validationMap)
|
|
{
|
|
if (added.find(nodeId) != added.end())
|
|
{
|
|
validation.setTrusted();
|
|
}
|
|
else if (removed.find(nodeId) != removed.end())
|
|
{
|
|
validation.setUntrusted();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
json::Value
|
|
getJsonTrie() const
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
return trie_.getJson();
|
|
}
|
|
|
|
/**
|
|
* Return the sequence number and ID of the preferred working ledger
|
|
*
|
|
* 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 curr The local node's current working ledger
|
|
*
|
|
* @return The sequence and id of the preferred working ledger,
|
|
* or std::nullopt if no trusted validations are available to
|
|
* determine the preferred ledger.
|
|
*/
|
|
std::optional<std::pair<Seq, ID>>
|
|
getPreferred(Ledger const& curr)
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
std::optional<SpanTip<Ledger>> preferred = withTrie(lock, [this](LedgerTrie<Ledger>& trie) {
|
|
return trie.getPreferred(localSeqEnforcer_.largest());
|
|
});
|
|
// No trusted validations to determine branch
|
|
if (!preferred)
|
|
{
|
|
// fall back to majority over acquiring ledgers
|
|
auto it = std::ranges::max_element(acquiring_, [](auto const& a, auto const& b) {
|
|
std::pair<Seq, ID> const& aKey = a.first;
|
|
typename hash_set<NodeID>::size_type const& aSize = a.second.size();
|
|
std::pair<Seq, ID> const& bKey = b.first;
|
|
typename hash_set<NodeID>::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::nullopt;
|
|
}
|
|
|
|
// 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::optional<std::pair<Seq, ID>> preferred = getPreferred(curr);
|
|
if (preferred && preferred->first >= minValidSeq)
|
|
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<ID, std::uint32_t> const& peerCounts)
|
|
{
|
|
std::optional<std::pair<Seq, ID>> preferred = getPreferred(lcl);
|
|
|
|
// Trusted validations exist, but stick with local preferred ledger if
|
|
// preferred is in the past
|
|
if (preferred)
|
|
return (preferred->first >= minSeq) ? preferred->second : lcl.id();
|
|
|
|
// Otherwise, rely on peer ledgers
|
|
auto it = std::ranges::max_element(peerCounts, [](auto const& a, auto const& 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);
|
|
});
|
|
|
|
if (it != peerCounts.end())
|
|
return it->first;
|
|
return lcl.id();
|
|
}
|
|
|
|
/**
|
|
* Count the number of current trusted validators working on a ledger
|
|
* after the specified one.
|
|
*
|
|
* @param ledger The working ledger
|
|
* @param ledgerID The preferred ledger
|
|
* @return The number of current trusted validators working on a descendant
|
|
* of the preferred ledger
|
|
*
|
|
* @note If ledger.id() != ledgerID, only counts immediate child ledgers of
|
|
* ledgerID
|
|
*/
|
|
std::size_t
|
|
getNodesAfter(Ledger const& ledger, ID const& ledgerID)
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
|
|
// Use trie if ledger is the right one
|
|
if (ledger.id() == ledgerID)
|
|
{
|
|
return withTrie(lock, [&ledger](LedgerTrie<Ledger>& trie) {
|
|
return trie.branchSupport(ledger) - trie.tipSupport(ledger);
|
|
});
|
|
}
|
|
|
|
// Count parent ledgers as fallback
|
|
return std::ranges::count_if(lastLedger_, [&ledgerID](auto const& it) {
|
|
auto const& curr = it.second;
|
|
return curr.seq() > Seq{0} && curr[curr.seq() - Seq{1}] == ledgerID;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the currently trusted full validations
|
|
*
|
|
* @return Vector of validations from currently trusted validators
|
|
*/
|
|
std::vector<WrappedValidationType>
|
|
currentTrusted()
|
|
{
|
|
std::vector<WrappedValidationType> ret;
|
|
std::scoped_lock const lock{mutex_};
|
|
current(
|
|
lock,
|
|
[&](std::size_t numValidations) { ret.reserve(numValidations); },
|
|
[&](NodeID const&, Validation const& v) {
|
|
if (v.trusted() && v.full())
|
|
ret.push_back(v.unwrap());
|
|
});
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Get the set of node ids associated with current validations
|
|
*
|
|
* @return The set of node ids for active, listed validators
|
|
*/
|
|
auto
|
|
getCurrentNodeIDs() -> hash_set<NodeID>
|
|
{
|
|
hash_set<NodeID> ret;
|
|
std::scoped_lock const lock{mutex_};
|
|
current(
|
|
lock,
|
|
[&](std::size_t numValidations) { ret.reserve(numValidations); },
|
|
[&](NodeID const& nid, Validation const&) { ret.insert(nid); });
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* 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(ID const& ledgerID)
|
|
{
|
|
std::size_t count = 0;
|
|
std::scoped_lock const lock{mutex_};
|
|
byLedger(
|
|
lock,
|
|
ledgerID,
|
|
[&](std::size_t) {}, // nothing to reserve
|
|
[&](NodeID const&, Validation const& v) {
|
|
if (v.trusted() && v.full())
|
|
++count;
|
|
});
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Get trusted full validations for a specific ledger
|
|
*
|
|
* @param ledgerID The identifier of ledger of interest
|
|
* @param seq The sequence number of ledger of interest
|
|
* @return Trusted validations associated with ledger
|
|
*/
|
|
std::vector<WrappedValidationType>
|
|
getTrustedForLedger(ID const& ledgerID, Seq const& seq)
|
|
{
|
|
std::vector<WrappedValidationType> res;
|
|
std::scoped_lock const lock{mutex_};
|
|
byLedger(
|
|
lock,
|
|
ledgerID,
|
|
[&](std::size_t numValidations) { res.reserve(numValidations); },
|
|
[&](NodeID const&, Validation const& v) {
|
|
if (v.trusted() && v.full() && v.seq() == seq)
|
|
res.emplace_back(v.unwrap());
|
|
});
|
|
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* 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<std::uint32_t>
|
|
fees(ID const& ledgerID, std::uint32_t baseFee)
|
|
{
|
|
std::vector<std::uint32_t> res;
|
|
std::scoped_lock const lock{mutex_};
|
|
byLedger(
|
|
lock,
|
|
ledgerID,
|
|
[&](std::size_t numValidations) { res.reserve(numValidations); },
|
|
[&](NodeID const&, Validation const& v) {
|
|
if (v.trusted() && v.full())
|
|
{
|
|
std::optional<std::uint32_t> loadFee = v.loadFee();
|
|
if (loadFee)
|
|
{
|
|
res.push_back(*loadFee);
|
|
}
|
|
else
|
|
{
|
|
res.push_back(baseFee);
|
|
}
|
|
}
|
|
});
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Flush all current validations
|
|
*/
|
|
void
|
|
flush()
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
current_.clear();
|
|
}
|
|
|
|
/**
|
|
* Return quantity of lagging proposers, and remove online proposers
|
|
* for purposes of evaluating whether to pause.
|
|
*
|
|
* Laggards are the trusted proposers whose sequence number is lower
|
|
* than the sequence number from which our current pending proposal
|
|
* is based. Proposers from whom we have not received a validation for
|
|
* awhile are considered offline.
|
|
*
|
|
* Note: the trusted flag is not used in this evaluation because it's made
|
|
* redundant by checking the list of proposers.
|
|
*
|
|
* @param seq Our current sequence number.
|
|
* @param trustedKeys Public keys of trusted proposers.
|
|
* @return Quantity of laggards.
|
|
*/
|
|
std::size_t
|
|
laggards(Seq const seq, hash_set<NodeKey>& trustedKeys)
|
|
{
|
|
std::size_t laggards = 0;
|
|
|
|
current(
|
|
std::scoped_lock{mutex_},
|
|
[](std::size_t) {},
|
|
[&](NodeID const&, Validation const& v) {
|
|
if (adaptor_.now() < v.seenTime() + parms_.validationFRESHNESS &&
|
|
trustedKeys.find(v.key()) != trustedKeys.end())
|
|
{
|
|
trustedKeys.erase(v.key());
|
|
if (seq > v.seq())
|
|
++laggards;
|
|
}
|
|
});
|
|
|
|
return laggards;
|
|
}
|
|
|
|
std::size_t
|
|
sizeOfCurrentCache() const
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
return current_.size();
|
|
}
|
|
|
|
std::size_t
|
|
sizeOfSeqEnforcersCache() const
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
return seqEnforcers_.size();
|
|
}
|
|
|
|
std::size_t
|
|
sizeOfByLedgerCache() const
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
return byLedger_.size();
|
|
}
|
|
|
|
std::size_t
|
|
sizeOfBySequenceCache() const
|
|
{
|
|
std::scoped_lock const lock{mutex_};
|
|
return bySequence_.size();
|
|
}
|
|
};
|
|
|
|
} // namespace xrpl
|