mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Add validator list RPC commands (RIPD-1541):
In support of dynamic validator list, this changeset: 1. Adds a new `validator_list_expires` field to `server_info` that indicates when the current validator list will become stale. 2. Adds a new admin only `validator_lists` RPC that returns the current list of known validators and the most recent published validator lists. 3. Adds a new admin only `validator_sites` RPC that returns the list of configured validator publisher sites and when they were most recently queried.
This commit is contained in:
@@ -2148,6 +2148,29 @@ Json::Value NetworkOPsImp::getServerInfo (bool human, bool admin)
|
||||
info[jss::validation_quorum] = static_cast<Json::UInt>(
|
||||
app_.validators ().quorum ());
|
||||
|
||||
if (admin)
|
||||
{
|
||||
if (auto when = app_.validators().expires())
|
||||
{
|
||||
if (human)
|
||||
{
|
||||
if(*when == TimeKeeper::time_point::max())
|
||||
info[jss::validator_list_expires] = "never";
|
||||
else
|
||||
info[jss::validator_list_expires] = to_string(*when);
|
||||
}
|
||||
else
|
||||
info[jss::validator_list_expires] =
|
||||
static_cast<Json::UInt>(when->time_since_epoch().count());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (human)
|
||||
info[jss::validator_list_expires] = "unknown";
|
||||
else
|
||||
info[jss::validator_list_expires] = 0;
|
||||
}
|
||||
}
|
||||
info[jss::io_latency_ms] = static_cast<Json::UInt> (
|
||||
app_.getIOLatency().count());
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <ripple/basics/UnorderedContainers.h>
|
||||
#include <ripple/core/TimeKeeper.h>
|
||||
#include <ripple/crypto/csprng.h>
|
||||
#include <ripple/json/json_value.h>
|
||||
#include <ripple/protocol/PublicKey.h>
|
||||
#include <boost/iterator/counting_iterator.hpp>
|
||||
#include <boost/range/adaptors.hpp>
|
||||
@@ -40,6 +41,9 @@ enum class ListDisposition
|
||||
/// List is valid
|
||||
accepted = 0,
|
||||
|
||||
/// Same sequence as current list
|
||||
same_sequence,
|
||||
|
||||
/// List version is not supported
|
||||
unsupported_version,
|
||||
|
||||
@@ -50,9 +54,12 @@ enum class ListDisposition
|
||||
stale,
|
||||
|
||||
/// Invalid format or signature
|
||||
invalid,
|
||||
invalid
|
||||
};
|
||||
|
||||
std::string
|
||||
to_string(ListDisposition disposition);
|
||||
|
||||
/**
|
||||
Trusted Validators List
|
||||
-----------------------
|
||||
@@ -103,7 +110,7 @@ class ValidatorList
|
||||
bool available;
|
||||
std::vector<PublicKey> list;
|
||||
std::size_t sequence;
|
||||
std::size_t expiration;
|
||||
TimeKeeper::time_point expiration;
|
||||
};
|
||||
|
||||
ManifestCache& validatorManifests_;
|
||||
@@ -126,6 +133,9 @@ class ValidatorList
|
||||
|
||||
PublicKey localPubKey_;
|
||||
|
||||
// Currently supported version of publisher list format
|
||||
static constexpr std::uint32_t requiredListVersion = 1;
|
||||
|
||||
// The minimum number of listed validators required to allow removing
|
||||
// non-communicative validators from the trusted set. In other words, if the
|
||||
// number of listed validators is less, then use all of them in the
|
||||
@@ -135,6 +145,8 @@ class ValidatorList
|
||||
// tolerance isn't needed.
|
||||
std::size_t const BYZANTINE_THRESHOLD {32};
|
||||
|
||||
|
||||
|
||||
public:
|
||||
ValidatorList (
|
||||
ManifestCache& validatorManifests,
|
||||
@@ -318,6 +330,26 @@ public:
|
||||
for_each_listed (
|
||||
std::function<void(PublicKey const&, bool)> func) const;
|
||||
|
||||
/** Return the time when the validator list will expire
|
||||
|
||||
@note This may be a time in the past if a published list has not
|
||||
been updated since its expiration. It will be boost::none if any
|
||||
configured published list has not been fetched.
|
||||
|
||||
@par Thread Safety
|
||||
May be called concurrently
|
||||
*/
|
||||
boost::optional<TimeKeeper::time_point>
|
||||
expires() const;
|
||||
|
||||
/** Return a JSON representation of the state of the validator list
|
||||
|
||||
@par Thread Safety
|
||||
May be called concurrently
|
||||
*/
|
||||
Json::Value
|
||||
getJson() const;
|
||||
|
||||
private:
|
||||
/** Check response for trusted valid published list
|
||||
|
||||
@@ -374,10 +406,9 @@ ValidatorList::onConsensusStart (
|
||||
for (auto const& list : publisherLists_)
|
||||
{
|
||||
// Remove any expired published lists
|
||||
if (list.second.expiration &&
|
||||
list.second.expiration <=
|
||||
timeKeeper_.now().time_since_epoch().count())
|
||||
removePublisherList (list.first);
|
||||
if (TimeKeeper::time_point{} < list.second.expiration &&
|
||||
list.second.expiration <= timeKeeper_.now())
|
||||
removePublisherList(list.first);
|
||||
|
||||
if (! list.second.available)
|
||||
allListsAvailable = false;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <ripple/app/misc/detail/Work.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/basics/StringUtilities.h>
|
||||
#include <ripple/json/json_value.h>
|
||||
#include <boost/asio.hpp>
|
||||
#include <mutex>
|
||||
|
||||
@@ -68,10 +69,17 @@ private:
|
||||
|
||||
struct Site
|
||||
{
|
||||
struct Status
|
||||
{
|
||||
clock_type::time_point refreshed;
|
||||
ListDisposition disposition;
|
||||
};
|
||||
|
||||
std::string uri;
|
||||
parsedURL pUrl;
|
||||
std::chrono::minutes refreshInterval;
|
||||
clock_type::time_point nextRefresh;
|
||||
boost::optional<Status> lastRefreshStatus;
|
||||
};
|
||||
|
||||
boost::asio::io_service& ios_;
|
||||
@@ -146,6 +154,11 @@ public:
|
||||
void
|
||||
stop ();
|
||||
|
||||
/** Return JSON representation of configured validator sites
|
||||
*/
|
||||
Json::Value
|
||||
getJson() const;
|
||||
|
||||
private:
|
||||
/// Queue next site to be fetched
|
||||
void
|
||||
|
||||
@@ -21,11 +21,33 @@
|
||||
#include <ripple/basics/Slice.h>
|
||||
#include <ripple/basics/StringUtilities.h>
|
||||
#include <ripple/json/json_reader.h>
|
||||
#include <ripple/protocol/JsonFields.h>
|
||||
#include <beast/core/detail/base64.hpp>
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
std::string
|
||||
to_string(ListDisposition disposition)
|
||||
{
|
||||
switch (disposition)
|
||||
{
|
||||
case ListDisposition::accepted:
|
||||
return "accepted";
|
||||
case ListDisposition::same_sequence:
|
||||
return "same_sequence";
|
||||
case ListDisposition::unsupported_version:
|
||||
return "unsupported_version";
|
||||
case ListDisposition::untrusted:
|
||||
return "untrusted";
|
||||
case ListDisposition::stale:
|
||||
return "stale";
|
||||
case ListDisposition::invalid:
|
||||
return "invalid";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
ValidatorList::ValidatorList (
|
||||
ManifestCache& validatorManifests,
|
||||
ManifestCache& publisherManifests,
|
||||
@@ -150,8 +172,15 @@ ValidatorList::load (
|
||||
JLOG (j_.warn()) << "Duplicate node identity: " << match[1];
|
||||
continue;
|
||||
}
|
||||
publisherLists_[local].list.emplace_back (std::move(*id));
|
||||
publisherLists_[local].available = true;
|
||||
auto it = publisherLists_.emplace(
|
||||
std::piecewise_construct,
|
||||
std::forward_as_tuple(local),
|
||||
std::forward_as_tuple());
|
||||
// Config listed keys never expire
|
||||
if (it.second)
|
||||
it.first->second.expiration = TimeKeeper::time_point::max();
|
||||
it.first->second.list.emplace_back(std::move(*id));
|
||||
it.first->second.available = true;
|
||||
++count;
|
||||
}
|
||||
|
||||
@@ -169,7 +198,7 @@ ValidatorList::applyList (
|
||||
std::string const& signature,
|
||||
std::uint32_t version)
|
||||
{
|
||||
if (version != 1)
|
||||
if (version != requiredListVersion)
|
||||
return ListDisposition::unsupported_version;
|
||||
|
||||
boost::unique_lock<boost::shared_mutex> lock{mutex_};
|
||||
@@ -184,7 +213,8 @@ ValidatorList::applyList (
|
||||
Json::Value const& newList = list["validators"];
|
||||
publisherLists_[pubKey].available = true;
|
||||
publisherLists_[pubKey].sequence = list["sequence"].asUInt ();
|
||||
publisherLists_[pubKey].expiration = list["expiration"].asUInt ();
|
||||
publisherLists_[pubKey].expiration = TimeKeeper::time_point{
|
||||
TimeKeeper::duration{list["expiration"].asUInt()}};
|
||||
std::vector<PublicKey>& publisherList = publisherLists_[pubKey].list;
|
||||
|
||||
std::vector<PublicKey> oldList = publisherList;
|
||||
@@ -328,11 +358,14 @@ ValidatorList::verify (
|
||||
list.isMember("expiration") && list["expiration"].isInt() &&
|
||||
list.isMember("validators") && list["validators"].isArray())
|
||||
{
|
||||
auto const sequence = list["sequence"].asUInt ();
|
||||
auto const expiration = list["expiration"].asUInt ();
|
||||
if (sequence <= publisherLists_[pubKey].sequence ||
|
||||
expiration <= timeKeeper_.now().time_since_epoch().count())
|
||||
auto const sequence = list["sequence"].asUInt();
|
||||
auto const expiration = TimeKeeper::time_point{
|
||||
TimeKeeper::duration{list["expiration"].asUInt()}};
|
||||
if (sequence < publisherLists_[pubKey].sequence ||
|
||||
expiration <= timeKeeper_.now())
|
||||
return ListDisposition::stale;
|
||||
else if (sequence == publisherLists_[pubKey].sequence)
|
||||
return ListDisposition::same_sequence;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -427,6 +460,103 @@ ValidatorList::removePublisherList (PublicKey const& publisherKey)
|
||||
return true;
|
||||
}
|
||||
|
||||
boost::optional<TimeKeeper::time_point>
|
||||
ValidatorList::expires() const
|
||||
{
|
||||
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
|
||||
boost::optional<TimeKeeper::time_point> res{boost::none};
|
||||
for (auto const& p : publisherLists_)
|
||||
{
|
||||
// Unfetched
|
||||
if (p.second.expiration == TimeKeeper::time_point{})
|
||||
return boost::none;
|
||||
|
||||
// Earliest
|
||||
if (!res || p.second.expiration < *res)
|
||||
res = p.second.expiration;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
ValidatorList::getJson() const
|
||||
{
|
||||
Json::Value res(Json::objectValue);
|
||||
|
||||
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
|
||||
|
||||
res[jss::validation_quorum] = static_cast<Json::UInt>(quorum());
|
||||
|
||||
if (auto when = expires())
|
||||
{
|
||||
if (*when == TimeKeeper::time_point::max())
|
||||
res[jss::validator_list_expires] = "never";
|
||||
else
|
||||
res[jss::validator_list_expires] = to_string(*when);
|
||||
}
|
||||
else
|
||||
res[jss::validator_list_expires] = "unknown";
|
||||
|
||||
// Local static keys
|
||||
PublicKey local;
|
||||
Json::Value& jLocalStaticKeys =
|
||||
(res[jss::local_static_keys] = Json::arrayValue);
|
||||
auto it = publisherLists_.find(local);
|
||||
if (it != publisherLists_.end())
|
||||
{
|
||||
for (auto const& key : it->second.list)
|
||||
jLocalStaticKeys.append(
|
||||
toBase58(TokenType::TOKEN_NODE_PUBLIC, key));
|
||||
}
|
||||
|
||||
// Publisher lists
|
||||
Json::Value& jPublisherLists =
|
||||
(res[jss::publisher_lists] = Json::arrayValue);
|
||||
for (auto const& p : publisherLists_)
|
||||
{
|
||||
if(local == p.first)
|
||||
continue;
|
||||
Json::Value& curr = jPublisherLists.append(Json::objectValue);
|
||||
curr[jss::pubkey_publisher] = strHex(p.first);
|
||||
curr[jss::available] = p.second.available;
|
||||
if(p.second.expiration != TimeKeeper::time_point{})
|
||||
{
|
||||
curr[jss::seq] = static_cast<Json::UInt>(p.second.sequence);
|
||||
curr[jss::expiration] = to_string(p.second.expiration);
|
||||
curr[jss::version] = requiredListVersion;
|
||||
}
|
||||
Json::Value& keys = (curr[jss::list] = Json::arrayValue);
|
||||
for (auto const& key : p.second.list)
|
||||
{
|
||||
keys.append(toBase58(TokenType::TOKEN_NODE_PUBLIC, key));
|
||||
}
|
||||
}
|
||||
|
||||
// Trusted validator keys
|
||||
Json::Value& jValidatorKeys =
|
||||
(res[jss::trusted_validator_keys] = Json::arrayValue);
|
||||
for (auto const& k : trustedKeys_)
|
||||
{
|
||||
jValidatorKeys.append(toBase58(TokenType::TOKEN_NODE_PUBLIC, k));
|
||||
}
|
||||
|
||||
// signing keys
|
||||
Json::Value& jSigningKeys = (res[jss::signing_keys] = Json::objectValue);
|
||||
validatorManifests_.for_each_manifest(
|
||||
[&jSigningKeys, this](Manifest const& manifest) {
|
||||
|
||||
auto it = keyListings_.find(manifest.masterKey);
|
||||
if (it != keyListings_.end())
|
||||
{
|
||||
jSigningKeys[toBase58(
|
||||
TokenType::TOKEN_NODE_PUBLIC, manifest.masterKey)] =
|
||||
toBase58(TokenType::TOKEN_NODE_PUBLIC, manifest.signingKey);
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
ValidatorList::for_each_listed (
|
||||
std::function<void(PublicKey const&, bool)> func) const
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/misc/detail/WorkPlain.h>
|
||||
#include <ripple/app/misc/detail/WorkSSL.h>
|
||||
#include <ripple/app/misc/ValidatorList.h>
|
||||
#include <ripple/app/misc/ValidatorSite.h>
|
||||
#include <ripple/app/misc/detail/WorkPlain.h>
|
||||
#include <ripple/app/misc/detail/WorkSSL.h>
|
||||
#include <ripple/basics/Slice.h>
|
||||
#include <ripple/json/json_reader.h>
|
||||
#include <ripple/protocol/JsonFields.h>
|
||||
#include <beast/core/detail/base64.hpp>
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
@@ -211,6 +212,9 @@ ValidatorSite::onSiteFetch(
|
||||
JLOG (j_.warn()) <<
|
||||
"Request for validator list at " <<
|
||||
sites_[siteIdx].uri << " returned " << res.result_int();
|
||||
|
||||
sites_[siteIdx].lastRefreshStatus.emplace(
|
||||
Site::Status{clock_type::now(), ListDisposition::invalid});
|
||||
}
|
||||
else if (! ec)
|
||||
{
|
||||
@@ -231,12 +235,21 @@ ValidatorSite::onSiteFetch(
|
||||
body["signature"].asString(),
|
||||
body["version"].asUInt());
|
||||
|
||||
sites_[siteIdx].lastRefreshStatus.emplace(
|
||||
Site::Status{clock_type::now(), disp});
|
||||
|
||||
if (ListDisposition::accepted == disp)
|
||||
{
|
||||
JLOG (j_.debug()) <<
|
||||
"Applied new validator list from " <<
|
||||
sites_[siteIdx].uri;
|
||||
}
|
||||
else if (ListDisposition::same_sequence == disp)
|
||||
{
|
||||
JLOG (j_.debug()) <<
|
||||
"Validator list with current sequence from " <<
|
||||
sites_[siteIdx].uri;
|
||||
}
|
||||
else if (ListDisposition::stale == disp)
|
||||
{
|
||||
JLOG (j_.warn()) <<
|
||||
@@ -277,10 +290,17 @@ ValidatorSite::onSiteFetch(
|
||||
JLOG (j_.warn()) <<
|
||||
"Unable to parse JSON response from " <<
|
||||
sites_[siteIdx].uri;
|
||||
|
||||
sites_[siteIdx].lastRefreshStatus.emplace(
|
||||
Site::Status{clock_type::now(), ListDisposition::invalid});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::lock_guard <std::mutex> lock{sites_mutex_};
|
||||
sites_[siteIdx].lastRefreshStatus.emplace(
|
||||
Site::Status{clock_type::now(), ListDisposition::invalid});
|
||||
|
||||
JLOG (j_.warn()) <<
|
||||
"Problem retrieving from " <<
|
||||
sites_[siteIdx].uri <<
|
||||
@@ -297,4 +317,32 @@ ValidatorSite::onSiteFetch(
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
Json::Value
|
||||
ValidatorSite::getJson() const
|
||||
{
|
||||
using namespace std::chrono;
|
||||
using Int = Json::Value::Int;
|
||||
|
||||
Json::Value jrr(Json::objectValue);
|
||||
Json::Value& jSites = (jrr[jss::validator_sites] = Json::arrayValue);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{sites_mutex_};
|
||||
for (Site const& site : sites_)
|
||||
{
|
||||
Json::Value& v = jSites.append(Json::objectValue);
|
||||
v[jss::uri] = site.uri;
|
||||
if (site.lastRefreshStatus)
|
||||
{
|
||||
v[jss::last_refresh_time] =
|
||||
to_string(site.lastRefreshStatus->refreshed);
|
||||
v[jss::last_refresh_status] =
|
||||
to_string(site.lastRefreshStatus->disposition);
|
||||
}
|
||||
|
||||
v[jss::refresh_interval_min] =
|
||||
static_cast<Int>(site.refreshInterval.count());
|
||||
}
|
||||
}
|
||||
return jrr;
|
||||
}
|
||||
} // ripple
|
||||
|
||||
Reference in New Issue
Block a user