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:
Brad Chase
2017-10-05 10:52:38 -04:00
parent 02059a27d6
commit 044dd53513
18 changed files with 1140 additions and 201 deletions

View File

@@ -3025,6 +3025,14 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\handlers\ValidatorListSites.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\handlers\Validators.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClInclude Include="..\..\src\ripple\rpc\handlers\Version.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\rpc\handlers\WalletPropose.cpp">
@@ -4761,6 +4769,8 @@
</ClInclude>
<ClInclude Include="..\..\src\test\jtx\trust.h">
</ClInclude>
<ClInclude Include="..\..\src\test\jtx\TrustedPublisherServer.h">
</ClInclude>
<ClInclude Include="..\..\src\test\jtx\txflags.h">
</ClInclude>
<ClInclude Include="..\..\src\test\jtx\utility.h">
@@ -5047,6 +5057,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\test\rpc\ValidatorRPC_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\test\server\ServerStatus_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>

View File

@@ -3630,6 +3630,12 @@
<ClCompile Include="..\..\src\ripple\rpc\handlers\ValidationSeed.cpp">
<Filter>ripple\rpc\handlers</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\handlers\ValidatorListSites.cpp">
<Filter>ripple\rpc\handlers</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\handlers\Validators.cpp">
<Filter>ripple\rpc\handlers</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ripple\rpc\handlers\Version.h">
<Filter>ripple\rpc\handlers</Filter>
</ClInclude>
@@ -5523,6 +5529,9 @@
<ClInclude Include="..\..\src\test\jtx\trust.h">
<Filter>test\jtx</Filter>
</ClInclude>
<ClInclude Include="..\..\src\test\jtx\TrustedPublisherServer.h">
<Filter>test\jtx</Filter>
</ClInclude>
<ClInclude Include="..\..\src\test\jtx\txflags.h">
<Filter>test\jtx</Filter>
</ClInclude>
@@ -5736,6 +5745,9 @@
<ClCompile Include="..\..\src\test\rpc\TransactionHistory_test.cpp">
<Filter>test\rpc</Filter>
</ClCompile>
<ClCompile Include="..\..\src\test\rpc\ValidatorRPC_test.cpp">
<Filter>test\rpc</Filter>
</ClCompile>
<ClCompile Include="..\..\src\test\server\ServerStatus_test.cpp">
<Filter>test\server</Filter>
</ClCompile>

View File

@@ -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());

View File

@@ -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,9 +406,8 @@ 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())
if (TimeKeeper::time_point{} < list.second.expiration &&
list.second.expiration <= timeKeeper_.now())
removePublisherList(list.first);
if (! list.second.available)

View File

@@ -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

View File

@@ -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;
@@ -329,10 +359,13 @@ ValidatorList::verify (
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 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

View File

@@ -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

View File

@@ -87,6 +87,7 @@ JSS ( assets ); // out: GatewayBalances
JSS ( authorized ); // out: AccountLines
JSS ( auth_change ); // out: AccountInfo
JSS ( auth_change_queued ); // out: AccountInfo
JSS ( available ); // out: ValidatorList
JSS ( balance ); // out: AccountLines
JSS ( balances ); // out: GatewayBalances
JSS ( base ); // out: LogLevel
@@ -124,7 +125,7 @@ JSS ( complete_ledgers ); // out: NetworkOPs, PeerImp
JSS ( consensus ); // out: NetworkOPs, LedgerConsensus
JSS ( converge_time ); // out: NetworkOPs
JSS ( converge_time_s ); // out: NetworkOPs
JSS ( count ); // in: AccountTx*
JSS ( count ); // in: AccountTx*, ValidatorList
JSS ( currency ); // in: paths/PathRequest, STAmount
// out: paths/Node, STPathSet, STAmount
JSS ( current ); // out: OwnerInfo
@@ -162,7 +163,8 @@ JSS ( error_message ); // out: error
JSS ( escrow ); // in: LedgerEntry
JSS ( expand ); // in: handler/Ledger
JSS ( expected_ledger_size ); // out: TxQ
JSS ( expiration ); // out: AccountOffers, AccountChannels
JSS ( expiration ); // out: AccountOffers, AccountChannels,
// ValidatorList
JSS ( fail_hard ); // in: Sign, Submit
JSS ( failed ); // out: InboundLedger
JSS ( feature ); // in: Feature
@@ -218,6 +220,8 @@ JSS ( key_type ); // in/out: WalletPropose, TransactionSign
JSS ( latency ); // out: PeerImp
JSS ( last ); // out: RPCVersion
JSS ( last_close ); // out: NetworkOPs
JSS ( last_refresh_time ); // out: ValidatorSite
JSS ( last_refresh_status ); // out: ValidatorSite
JSS ( ledger ); // in: NetworkOPs, LedgerCleaner,
// RPCHelpers
// out: NetworkOPs, PeerImp
@@ -242,6 +246,7 @@ JSS ( limit ); // in/out: AccountTx*, AccountOffers,
// in: LedgerData, BookOffers
JSS ( limit_peer ); // out: AccountLines
JSS ( lines ); // out: AccountLines
JSS ( list ); // out: ValidatorList
JSS ( load ); // out: NetworkOPs, PeerImp
JSS ( load_base ); // out: NetworkOPs
JSS ( load_factor ); // out: NetworkOPs
@@ -255,6 +260,7 @@ JSS ( load_factor_server ); // out: NetworkOPs
JSS ( load_fee ); // out: LoadFeeTrackImp, NetworkOPs
JSS ( local ); // out: resource/Logic.h
JSS ( local_txs ); // out: GetCounts
JSS ( local_static_keys ); // out: ValidatorList
JSS ( lowest_sequence ); // out: AccountInfo
JSS ( majority ); // out: RPC feature
JSS ( marker ); // in/out: AccountTx, AccountOffers,
@@ -327,10 +333,12 @@ JSS ( propose_seq ); // out: LedgerPropose
JSS ( proposers ); // out: NetworkOPs, LedgerConsensus
JSS ( protocol ); // out: PeerImp
JSS ( pubkey_node ); // out: NetworkOPs
JSS ( pubkey_validator ); // out: NetworkOPs
JSS ( pubkey_publisher ); // out: ValidatorList
JSS ( pubkey_validator ); // out: NetworkOPs, ValidatorList
JSS ( public_key ); // out: OverlayImpl, PeerImp, WalletPropose
JSS ( public_key_hex ); // out: WalletPropose
JSS ( published_ledger ); // out: NetworkOPs
JSS ( publisher_lists ); // out: ValidatorList
JSS ( quality ); // out: NetworkOPs
JSS ( quality_in ); // out: AccountLines
JSS ( quality_out ); // out: AccountLines
@@ -340,6 +348,7 @@ JSS ( random ); // out: Random
JSS ( raw_meta ); // out: AcceptedLedgerTx
JSS ( receive_currencies ); // out: AccountCurrencies
JSS ( reference_level ); // out: TxQ
JSS ( refresh_interval_min ); // out: ValidatorSites
JSS ( regular_seed ); // in/out: LedgerEntry
JSS ( remote ); // out: Logic.h
JSS ( request ); // RPC
@@ -364,7 +373,8 @@ JSS ( seed_hex ); // in: WalletPropose, TransactionSign
JSS ( send_currencies ); // out: AccountCurrencies
JSS ( send_max ); // in: PathRequest, RipplePathFind
JSS ( seq ); // in: LedgerEntry;
// out: NetworkOPs, RPCSub, AccountOffers
// out: NetworkOPs, RPCSub, AccountOffers,
// ValidatorList
JSS ( seqNum ); // out: LedgerToJson
JSS ( server_state ); // out: NetworkOPs
JSS ( server_status ); // out: NetworkOPs
@@ -373,6 +383,7 @@ JSS ( severity ); // in: LogLevel
JSS ( signature ); // out: NetworkOPs, ChannelAuthorize
JSS ( signature_verified ); // out: ChannelVerify
JSS ( signing_key ); // out: NetworkOPs
JSS ( signing_keys ); // out: ValidatorList
JSS ( signing_time ); // out: NetworkOPs
JSS ( signer_list ); // in: AccountObjects
JSS ( signer_lists ); // in/out: AccountInfo
@@ -417,6 +428,7 @@ JSS ( transitions ); // out: NetworkOPs
JSS ( treenode_cache_size ); // out: GetCounts
JSS ( treenode_track_size ); // out: GetCounts
JSS ( trusted ); // out: UnlList
JSS ( trusted_validator_keys ); // out: ValidatorList
JSS ( tx ); // out: STTx, AccountTx*
JSS ( tx_blob ); // in/out: Submit,
// in: TransactionSign, AccountTx*
@@ -434,6 +446,7 @@ JSS ( type_hex ); // out: STPathSet
JSS ( unl ); // out: UnlList
JSS ( unlimited); // out: Connection.h
JSS ( uptime ); // out: GetCounts
JSS ( uri ); // out: ValidatorSites
JSS ( url ); // in/out: Subscribe, Unsubscribe
JSS ( url_password ); // in: Subscribe
JSS ( url_username ); // in: Subscribe
@@ -441,6 +454,7 @@ JSS ( urlgravatar ); //
JSS ( username ); // in: Subscribe
JSS ( validated ); // out: NetworkOPs, RPCHelpers, AccountTx*
// Tx
JSS ( validator_list_expires ); // out: NetworkOps, ValidatorList
JSS ( validated_ledger ); // out: NetworkOPs
JSS ( validated_ledgers ); // out: NetworkOPs
JSS ( validation_key ); // out: ValidationCreate, ValidationSeed
@@ -449,6 +463,7 @@ JSS ( validation_public_key ); // out: ValidationCreate, ValidationSeed
JSS ( validation_quorum ); // out: NetworkOPs
JSS ( validation_seed ); // out: ValidationCreate, ValidationSeed
JSS ( validations ); // out: AmendmentTableImpl
JSS ( validator_sites ); // out: ValidatorSites
JSS ( value ); // out: STAmount
JSS ( version ); // out: RPCVersion
JSS ( vetoed ); // out: AmendmentTableImpl

View File

@@ -85,7 +85,8 @@ Json::Value doWalletPropose (RPC::Context&);
Json::Value doWalletSeed (RPC::Context&);
Json::Value doWalletUnlock (RPC::Context&);
Json::Value doWalletVerify (RPC::Context&);
Json::Value doValidators (RPC::Context&);
Json::Value doValidatorListSites (RPC::Context&);
} // ripple
#endif

View File

@@ -0,0 +1,33 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2014 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 <BeastConfig.h>
#include <ripple/app/main/Application.h>
#include <ripple/app/misc/ValidatorSite.h>
#include <ripple/rpc/Context.h>
namespace ripple {
Json::Value
doValidatorListSites(RPC::Context& context)
{
return context.app.validatorSites().getJson();
}
} // namespace ripple

View File

@@ -0,0 +1,33 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2014 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 <BeastConfig.h>
#include <ripple/app/main/Application.h>
#include <ripple/app/misc/ValidatorList.h>
#include <ripple/rpc/Context.h>
namespace ripple {
Json::Value
doValidators(RPC::Context& context)
{
return context.app.validators().getJson();
}
} // namespace ripple

View File

@@ -151,6 +151,8 @@ Handler handlerArray[] {
{ "unl_list", byRef (&doUnlList), Role::ADMIN, NO_CONDITION },
{ "validation_create", byRef (&doValidationCreate), Role::ADMIN, NO_CONDITION },
{ "validation_seed", byRef (&doValidationSeed), Role::ADMIN, NO_CONDITION },
{ "validators", byRef (&doValidators), Role::ADMIN, NO_CONDITION },
{ "validator_list_sites", byRef (&doValidatorListSites), Role::ADMIN, NO_CONDITION },
{ "wallet_propose", byRef (&doWalletPropose), Role::ADMIN, NO_CONDITION },
{ "wallet_seed", byRef (&doWalletSeed), Role::ADMIN, NO_CONDITION },

View File

@@ -48,6 +48,8 @@
#include <ripple/rpc/handlers/Unsubscribe.cpp>
#include <ripple/rpc/handlers/ValidationCreate.cpp>
#include <ripple/rpc/handlers/ValidationSeed.cpp>
#include <ripple/rpc/handlers/Validators.cpp>
#include <ripple/rpc/handlers/ValidatorListSites.cpp>
#include <ripple/rpc/handlers/WalletPropose.cpp>
#include <ripple/rpc/handlers/WalletSeed.cpp>
@@ -59,3 +61,5 @@
#include <ripple/rpc/impl/ServerHandlerImp.cpp>
#include <ripple/rpc/impl/Status.cpp>
#include <ripple/rpc/impl/TransactionSign.cpp>

View File

@@ -456,7 +456,7 @@ private:
trustedKeys->applyList (
manifest1, blob1, sig1, version));
BEAST_EXPECT(ListDisposition::stale ==
BEAST_EXPECT(ListDisposition::same_sequence ==
trustedKeys->applyList (
manifest1, blob2, sig2, version));
@@ -941,6 +941,124 @@ private:
}
}
void
testExpires()
{
testcase("Expires");
beast::Journal journal;
jtx::Env env(*this);
auto toStr = [](PublicKey const& publicKey) {
return toBase58(TokenType::TOKEN_NODE_PUBLIC, publicKey);
};
// Config listed keys
{
ManifestCache manifests;
auto trustedKeys = std::make_unique<ValidatorList>(
manifests, manifests, env.timeKeeper(), journal);
// Empty list has no expiration
BEAST_EXPECT(trustedKeys->expires() == boost::none);
// Config listed keys have maximum expiry
PublicKey emptyLocalKey;
PublicKey localCfgListed = randomNode();
trustedKeys->load(emptyLocalKey, {toStr(localCfgListed)}, {});
BEAST_EXPECT(
trustedKeys->expires() &&
trustedKeys->expires().get() == NetClock::time_point::max());
BEAST_EXPECT(trustedKeys->listed(localCfgListed));
}
// Published keys with expirations
{
ManifestCache manifests;
auto trustedKeys = std::make_unique<ValidatorList>(
manifests, manifests, env.app().timeKeeper(), journal);
std::vector<Validator> validators = {randomValidator()};
hash_set<PublicKey> activeKeys;
for(Validator const & val : validators)
activeKeys.insert(val.masterPublic);
// Store prepared list data to control when it is applied
struct PreparedList
{
std::string manifest;
std::string blob;
std::string sig;
int version;
NetClock::time_point expiration;
};
auto addPublishedList = [this, &env, &trustedKeys, &validators]()
{
auto const publisherSecret = randomSecretKey();
auto const publisherPublic =
derivePublicKey(KeyType::ed25519, publisherSecret);
auto const pubSigningKeys = randomKeyPair(KeyType::secp256k1);
auto const manifest = beast::detail::base64_encode(makeManifestString (
publisherPublic, publisherSecret,
pubSigningKeys.first, pubSigningKeys.second, 1));
std::vector<std::string> cfgPublishers({
strHex(publisherPublic)});
PublicKey emptyLocalKey;
std::vector<std::string> emptyCfgKeys;
BEAST_EXPECT(trustedKeys->load (
emptyLocalKey, emptyCfgKeys, cfgPublishers));
auto const version = 1;
auto const sequence = 1;
NetClock::time_point const expiration =
env.timeKeeper().now() + 3600s;
auto const blob = makeList(
validators,
sequence,
expiration.time_since_epoch().count());
auto const sig = signList (blob, pubSigningKeys);
return PreparedList{manifest, blob, sig, version, expiration};
};
// Configure two publishers and prepare 2 lists
PreparedList prep1 = addPublishedList();
env.timeKeeper().set(env.timeKeeper().now() + 200s);
PreparedList prep2 = addPublishedList();
// Initially, no list has been published, so no known expiration
BEAST_EXPECT(trustedKeys->expires() == boost::none);
// Apply first list
BEAST_EXPECT(
ListDisposition::accepted == trustedKeys->applyList(
prep1.manifest, prep1.blob, prep1.sig, prep1.version));
// One list still hasn't published, so expiration is still unknown
BEAST_EXPECT(trustedKeys->expires() == boost::none);
// Apply second list
BEAST_EXPECT(
ListDisposition::accepted == trustedKeys->applyList(
prep2.manifest, prep2.blob, prep2.sig, prep2.version));
// We now have loaded both lists, so expiration is known
BEAST_EXPECT(
trustedKeys->expires() &&
trustedKeys->expires().get() == prep1.expiration);
// Advance past the first list's expiration, but it remains the
// earliest expiration
env.timeKeeper().set(prep1.expiration + 1s);
trustedKeys->onConsensusStart(activeKeys);
BEAST_EXPECT(
trustedKeys->expires() &&
trustedKeys->expires().get() == prep1.expiration);
}
}
public:
void
run() override
@@ -949,6 +1067,7 @@ public:
testConfigLoad ();
testApplyList ();
testUpdate ();
testExpires ();
}
};

View File

@@ -18,7 +18,6 @@
//==============================================================================
#include <beast/core/detail/base64.hpp>
#include <beast/http.hpp>
#include <ripple/app/misc/ValidatorSite.h>
#include <ripple/basics/Slice.h>
#include <ripple/basics/strHex.h>
@@ -28,181 +27,18 @@
#include <ripple/protocol/SecretKey.h>
#include <ripple/protocol/Sign.h>
#include <test/jtx.h>
#include <boost/utility/in_place_factory.hpp>
#include <test/jtx/TrustedPublisherServer.h>
#include <boost/asio.hpp>
namespace ripple {
namespace test {
struct Validator
{
PublicKey masterPublic;
PublicKey signingPublic;
std::string manifest;
};
class http_sync_server
{
using endpoint_type = boost::asio::ip::tcp::endpoint;
using address_type = boost::asio::ip::address;
using socket_type = boost::asio::ip::tcp::socket;
using req_type = beast::http::request<beast::http::string_body>;
using resp_type = beast::http::response<beast::http::string_body>;
using error_code = boost::system::error_code;
socket_type sock_;
boost::asio::ip::tcp::acceptor acceptor_;
std::string list_;
public:
http_sync_server(endpoint_type const& ep,
boost::asio::io_service& ios,
std::pair<PublicKey, SecretKey> keys,
std::string const& manifest,
int sequence,
std::size_t expiration,
int version,
std::vector <Validator> const& validators)
: sock_(ios)
, acceptor_(ios)
{
std::string data =
"{\"sequence\":" + std::to_string(sequence) +
",\"expiration\":" + std::to_string(expiration) +
",\"validators\":[";
for (auto const& val : validators)
{
data += "{\"validation_public_key\":\"" + strHex(val.masterPublic) +
"\",\"manifest\":\"" + val.manifest + "\"},";
}
data.pop_back();
data += "]}";
std::string blob = beast::detail::base64_encode(data);
list_ = "{\"blob\":\"" + blob + "\"";
auto const sig = sign(keys.first, keys.second, makeSlice(data));
list_ += ",\"signature\":\"" + strHex(sig) + "\"";
list_ += ",\"manifest\":\"" + manifest + "\"";
list_ += ",\"version\":" + std::to_string(version) + '}';
acceptor_.open(ep.protocol());
error_code ec;
acceptor_.set_option(
boost::asio::ip::tcp::acceptor::reuse_address(true), ec);
acceptor_.bind(ep);
acceptor_.listen(boost::asio::socket_base::max_connections);
acceptor_.async_accept(sock_,
std::bind(&http_sync_server::on_accept, this,
std::placeholders::_1));
}
~http_sync_server()
{
error_code ec;
acceptor_.close(ec);
}
private:
struct lambda
{
int id;
http_sync_server& self;
socket_type sock;
boost::asio::io_service::work work;
lambda(int id_, http_sync_server& self_,
socket_type&& sock_)
: id(id_)
, self(self_)
, sock(std::move(sock_))
, work(sock.get_io_service())
{
}
void operator()()
{
self.do_peer(id, std::move(sock));
}
};
void
on_accept(error_code ec)
{
// ec must be checked before `acceptor_` or the member variable may be
// accessed after the destructor has completed
if(ec || !acceptor_.is_open())
return;
static int id_ = 0;
std::thread{lambda{++id_, *this, std::move(sock_)}}.detach();
acceptor_.async_accept(sock_,
std::bind(&http_sync_server::on_accept, this,
std::placeholders::_1));
}
void
do_peer(int id, socket_type&& sock0)
{
socket_type sock(std::move(sock0));
beast::multi_buffer sb;
error_code ec;
for(;;)
{
req_type req;
beast::http::read(sock, sb, req, ec);
if(ec)
break;
auto path = req.target().to_string();
if(path != "/validators")
{
resp_type res;
res.result(beast::http::status::not_found);
res.version = req.version;
res.insert("Server", "http_sync_server");
res.insert("Content-Type", "text/html");
res.body = "The file '" + path + "' was not found";
res.prepare_payload();
write(sock, res, ec);
if(ec)
break;
}
resp_type res;
res.result(beast::http::status::ok);
res.version = req.version;
res.insert("Server", "http_sync_server");
res.insert("Content-Type", "application/json");
res.body = list_;
try
{
res.prepare_payload();
}
catch(std::exception const& e)
{
res = {};
res.result(beast::http::status::internal_server_error);
res.version = req.version;
res.insert("Server", "http_sync_server");
res.insert("Content-Type", "text/html");
res.body =
std::string{"An internal error occurred"} + e.what();
res.prepare_payload();
}
write(sock, res, ec);
if(ec)
break;
}
}
};
class ValidatorSite_test : public beast::unit_test::suite
{
private:
using Validator = TrustedPublisherServer::Validator;
static
PublicKey
randomNode ()
@@ -293,7 +129,6 @@ private:
using namespace jtx;
Env env (*this);
auto& ioService = env.app ().getIOService ();
auto& trustedKeys = env.app ().validators ();
beast::Journal journal;
@@ -337,27 +172,42 @@ private:
while (list2.size () < listSize)
list2.push_back (randomValidator());
std::uint16_t constexpr port1 = 7475;
std::uint16_t constexpr port2 = 7476;
using endpoint_type = boost::asio::ip::tcp::endpoint;
using address_type = boost::asio::ip::address;
endpoint_type ep1{address_type::from_string("127.0.0.1"), port1};
endpoint_type ep2{address_type::from_string("127.0.0.1"), port2};
// Use ports of 0 to allow OS selection
endpoint_type ep1{address_type::from_string("127.0.0.1"), 0};
endpoint_type ep2{address_type::from_string("127.0.0.1"), 0};
auto const sequence = 1;
auto const version = 1;
NetClock::time_point const expiration =
env.timeKeeper().now() + 3600s;
http_sync_server server1(
ep1, ioService, pubSigningKeys1, manifest1, sequence,
expiration.time_since_epoch().count(), version, list1);
TrustedPublisherServer server1(
ep1,
env.app().getIOService(),
pubSigningKeys1,
manifest1,
sequence,
expiration,
version,
list1);
TrustedPublisherServer server2(
ep2,
env.app().getIOService(),
pubSigningKeys2,
manifest2,
sequence,
expiration,
version,
list2);
std::uint16_t const port1 = server1.local_endpoint().port();
std::uint16_t const port2 = server2.local_endpoint().port();
http_sync_server server2(
ep2, ioService, pubSigningKeys2, manifest2, sequence,
expiration.time_since_epoch().count(), version, list2);
{
// fetch single site

View File

@@ -0,0 +1,210 @@
//------------------------------------------------------------------------------
/*
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.
*/
//==============================================================================
#ifndef RIPPLE_TEST_TRUSTED_PUBLISHER_SERVER_H_INCLUDED
#define RIPPLE_TEST_TRUSTED_PUBLISHER_SERVER_H_INCLUDED
#include <beast/core/detail/base64.hpp>
#include <ripple/protocol/PublicKey.h>
#include <ripple/protocol/SecretKey.h>
#include <ripple/protocol/Sign.h>
#include <ripple/basics/strHex.h>
#include <boost/asio.hpp>
#include <beast/core/detail/base64.hpp>
#include <beast/http.hpp>
namespace ripple {
namespace test {
class TrustedPublisherServer
{
using endpoint_type = boost::asio::ip::tcp::endpoint;
using address_type = boost::asio::ip::address;
using socket_type = boost::asio::ip::tcp::socket;
using req_type = beast::http::request<beast::http::string_body>;
using resp_type = beast::http::response<beast::http::string_body>;
using error_code = boost::system::error_code;
socket_type sock_;
boost::asio::ip::tcp::acceptor acceptor_;
std::string list_;
public:
struct Validator
{
PublicKey masterPublic;
PublicKey signingPublic;
std::string manifest;
};
TrustedPublisherServer(
endpoint_type const& ep,
boost::asio::io_service& ios,
std::pair<PublicKey, SecretKey> keys,
std::string const& manifest,
int sequence,
NetClock::time_point expiration,
int version,
std::vector<Validator> const& validators)
: sock_(ios), acceptor_(ios)
{
std::string data = "{\"sequence\":" + std::to_string(sequence) +
",\"expiration\":" +
std::to_string(expiration.time_since_epoch().count()) +
",\"validators\":[";
for (auto const& val : validators)
{
data += "{\"validation_public_key\":\"" + strHex(val.masterPublic) +
"\",\"manifest\":\"" + val.manifest + "\"},";
}
data.pop_back();
data += "]}";
std::string blob = beast::detail::base64_encode(data);
list_ = "{\"blob\":\"" + blob + "\"";
auto const sig = sign(keys.first, keys.second, makeSlice(data));
list_ += ",\"signature\":\"" + strHex(sig) + "\"";
list_ += ",\"manifest\":\"" + manifest + "\"";
list_ += ",\"version\":" + std::to_string(version) + '}';
acceptor_.open(ep.protocol());
error_code ec;
acceptor_.set_option(
boost::asio::ip::tcp::acceptor::reuse_address(true), ec);
acceptor_.bind(ep);
acceptor_.listen(boost::asio::socket_base::max_connections);
acceptor_.async_accept(
sock_,
std::bind(
&TrustedPublisherServer::on_accept, this, std::placeholders::_1));
}
~TrustedPublisherServer()
{
error_code ec;
acceptor_.close(ec);
}
endpoint_type
local_endpoint() const
{
return acceptor_.local_endpoint();
}
private:
struct lambda
{
int id;
TrustedPublisherServer& self;
socket_type sock;
boost::asio::io_service::work work;
lambda(int id_, TrustedPublisherServer& self_, socket_type&& sock_)
: id(id_)
, self(self_)
, sock(std::move(sock_))
, work(sock.get_io_service())
{
}
void
operator()()
{
self.do_peer(id, std::move(sock));
}
};
void
on_accept(error_code ec)
{
// ec must be checked before `acceptor_` or the member variable may be
// accessed after the destructor has completed
if (ec || !acceptor_.is_open())
return;
static int id_ = 0;
std::thread{lambda{++id_, *this, std::move(sock_)}}.detach();
acceptor_.async_accept(
sock_,
std::bind(
&TrustedPublisherServer::on_accept, this, std::placeholders::_1));
}
void
do_peer(int id, socket_type&& sock0)
{
socket_type sock(std::move(sock0));
beast::multi_buffer sb;
error_code ec;
for (;;)
{
req_type req;
beast::http::read(sock, sb, req, ec);
if (ec)
break;
auto path = req.target().to_string();
if (path != "/validators")
{
resp_type res;
res.result(beast::http::status::not_found);
res.version = req.version;
res.insert("Server", "TrustedPublisherServer");
res.insert("Content-Type", "text/html");
res.body = "The file '" + path + "' was not found";
res.prepare_payload();
write(sock, res, ec);
if (ec)
break;
}
resp_type res;
res.result(beast::http::status::ok);
res.version = req.version;
res.insert("Server", "TrustedPublisherServer");
res.insert("Content-Type", "application/json");
res.body = list_;
try
{
res.prepare_payload();
}
catch (std::exception const& e)
{
res = {};
res.result(beast::http::status::internal_server_error);
res.version = req.version;
res.insert("Server", "TrustedPublisherServer");
res.insert("Content-Type", "text/html");
res.body = std::string{"An internal error occurred"} + e.what();
res.prepare_payload();
}
write(sock, res, ec);
if (ec)
break;
}
}
};
} // namespace test
} // namespace ripple
#endif

View File

@@ -0,0 +1,400 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2016 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <BeastConfig.h>
#include <ripple/app/main/BasicApp.h>
#include <ripple/app/misc/ValidatorSite.h>
#include <ripple/beast/unit_test.h>
#include <ripple/core/ConfigSections.h>
#include <ripple/json/json_value.h>
#include <ripple/protocol/JsonFields.h>
#include <ripple/protocol/Sign.h>
#include <beast/core/detail/base64.hpp>
#include <test/jtx.h>
#include <test/jtx/TrustedPublisherServer.h>
#include <set>
namespace ripple {
namespace test {
class ValidatorRPC_test : public beast::unit_test::suite
{
using Validator = TrustedPublisherServer::Validator;
static
Validator
randomValidator ()
{
auto const secret = randomSecretKey();
auto const masterPublic =
derivePublicKey(KeyType::ed25519, secret);
auto const signingKeys = randomKeyPair(KeyType::secp256k1);
return { masterPublic, signingKeys.first, makeManifestString (
masterPublic, secret, signingKeys.first, signingKeys.second, 1) };
}
static
std::string
makeManifestString(
PublicKey const& pk,
SecretKey const& sk,
PublicKey const& spk,
SecretKey const& ssk,
int seq)
{
STObject st(sfGeneric);
st[sfSequence] = seq;
st[sfPublicKey] = pk;
st[sfSigningPubKey] = spk;
sign(st, HashPrefix::manifest, *publicKeyType(spk), ssk);
sign(
st,
HashPrefix::manifest,
*publicKeyType(pk),
sk,
sfMasterSignature);
Serializer s;
st.add(s);
return beast::detail::base64_encode(
std::string(static_cast<char const*>(s.data()), s.size()));
}
public:
void
testPrivileges()
{
using namespace test::jtx;
for (bool const isAdmin : {true, false})
{
for (std::string cmd : {"validators", "validator_list_sites"})
{
Env env{*this, isAdmin ? envconfig() : envconfig(no_admin)};
auto const jrr = env.rpc(cmd)[jss::result];
if (isAdmin)
{
BEAST_EXPECT(!jrr.isMember(jss::error));
BEAST_EXPECT(jrr[jss::status] == "success");
}
else
{
// The current HTTP/S ServerHandler returns an HTTP 403
// error code here rather than a noPermission JSON error.
// The JSONRPCClient just eats that error and returns null
// result.
BEAST_EXPECT(jrr.isNull());
}
}
{
Env env{*this, isAdmin ? envconfig() : envconfig(no_admin)};
auto const jrr = env.rpc("server_info")[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
BEAST_EXPECT(jrr[jss::info].isMember(
jss::validator_list_expires) == isAdmin);
}
{
Env env{*this, isAdmin ? envconfig() : envconfig(no_admin)};
auto const jrr = env.rpc("server_state")[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
BEAST_EXPECT(jrr[jss::state].isMember(
jss::validator_list_expires) == isAdmin);
}
}
}
void
testStaticUNL()
{
using namespace test::jtx;
std::set<std::string> const keys = {
"n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7",
"n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj"};
Env env{
*this,
envconfig([&keys](std::unique_ptr<Config> cfg) {
for (auto const& key : keys)
cfg->section(SECTION_VALIDATORS).append(key);
return cfg;
}),
};
// Server info reports maximum expiration since not dynamic
{
auto const jrr = env.rpc("server_info")[jss::result];
BEAST_EXPECT(
jrr[jss::info][jss::validator_list_expires] == "never");
}
{
auto const jrr = env.rpc("server_state")[jss::result];
BEAST_EXPECT(
jrr[jss::state][jss::validator_list_expires].asUInt() ==
NetClock::time_point::max().time_since_epoch().count());
}
// All our keys are in the response
{
auto const jrr = env.rpc("validators")[jss::result];
BEAST_EXPECT(jrr[jss::validator_list_expires] == "never");
BEAST_EXPECT(jrr[jss::validation_quorum].asUInt() == keys.size());
BEAST_EXPECT(jrr[jss::trusted_validator_keys].size() == keys.size());
BEAST_EXPECT(jrr[jss::publisher_lists].size() == 0);
BEAST_EXPECT(jrr[jss::local_static_keys].size() == keys.size());
for (auto const& jKey : jrr[jss::local_static_keys])
{
BEAST_EXPECT(keys.count(jKey.asString())== 1);
}
BEAST_EXPECT(jrr[jss::signing_keys].size() == 0);
}
// No validator sites configured
{
auto const jrr = env.rpc("validator_list_sites")[jss::result];
BEAST_EXPECT(jrr[jss::validator_sites].size() == 0);
}
}
void
testDynamicUNL()
{
using namespace test::jtx;
using endpoint_type = boost::asio::ip::tcp::endpoint;
using address_type = boost::asio::ip::address;
auto toStr = [](PublicKey const& publicKey) {
return toBase58(TokenType::TOKEN_NODE_PUBLIC, publicKey);
};
// Publisher manifest/signing keys
auto const publisherSecret = randomSecretKey();
auto const publisherPublic =
derivePublicKey(KeyType::ed25519, publisherSecret);
auto const publisherSigningKeys = randomKeyPair(KeyType::secp256k1);
auto const manifest = makeManifestString(
publisherPublic,
publisherSecret,
publisherSigningKeys.first,
publisherSigningKeys.second,
1);
// Validator keys that will be in the published list
std::vector<Validator> validators = {randomValidator(), randomValidator()};
std::set<std::string> expectedKeys;
for (auto const& val : validators)
expectedKeys.insert(toStr(val.masterPublic));
//----------------------------------------------------------------------
// Publisher list site unavailable
{
// Publisher site information
std::string siteURI = "http://127.0.0.1:1234/validators";
Env env{
*this,
envconfig([&](std::unique_ptr<Config> cfg) {
cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI);
cfg->section(SECTION_VALIDATOR_LIST_KEYS)
.append(strHex(publisherPublic));
return cfg;
}),
};
env.app().validatorSites().start();
env.app().validatorSites().join();
{
auto const jrr = env.rpc("server_info")[jss::result];
BEAST_EXPECT(
jrr[jss::info][jss::validator_list_expires] == "unknown");
}
{
auto const jrr = env.rpc("server_state")[jss::result];
BEAST_EXPECT(
jrr[jss::state][jss::validator_list_expires].asInt() == 0);
}
{
auto const jrr = env.rpc("validators")[jss::result];
BEAST_EXPECT(jrr[jss::validation_quorum].asUInt() ==
std::numeric_limits<std::uint32_t>::max());
BEAST_EXPECT(jrr[jss::local_static_keys].size() == 0);
BEAST_EXPECT(jrr[jss::trusted_validator_keys].size() == 0);
BEAST_EXPECT(jrr[jss::validator_list_expires] == "unknown");
if (BEAST_EXPECT(jrr[jss::publisher_lists].size() == 1))
{
auto jp = jrr[jss::publisher_lists][0u];
BEAST_EXPECT(jp[jss::available] == false);
BEAST_EXPECT(jp[jss::list].size() == 0);
BEAST_EXPECT(!jp.isMember(jss::seq));
BEAST_EXPECT(!jp.isMember(jss::expiration));
BEAST_EXPECT(!jp.isMember(jss::version));
BEAST_EXPECT(
jp[jss::pubkey_publisher] == strHex(publisherPublic));
}
BEAST_EXPECT(jrr[jss::signing_keys].size() == 0);
}
{
auto const jrr = env.rpc("validator_list_sites")[jss::result];
if (BEAST_EXPECT(jrr[jss::validator_sites].size() == 1))
{
auto js = jrr[jss::validator_sites][0u];
BEAST_EXPECT(js[jss::refresh_interval_min].asUInt() == 5);
BEAST_EXPECT(js[jss::uri] == siteURI);
BEAST_EXPECT(js.isMember(jss::last_refresh_time));
BEAST_EXPECT(js[jss::last_refresh_status] == "invalid");
}
}
}
//----------------------------------------------------------------------
// Publisher list site available
{
NetClock::time_point const expiration{3600s};
// 0 port means to use OS port selection
endpoint_type ep{address_type::from_string("127.0.0.1"), 0};
// Manage single thread io_service for server
struct Worker : BasicApp
{
Worker() : BasicApp(1) {}
};
Worker w;
TrustedPublisherServer server(
ep,
w.get_io_service(),
publisherSigningKeys,
manifest,
1,
expiration,
1,
validators);
endpoint_type const & local_ep = server.local_endpoint();
std::string siteURI = "http://127.0.0.1:" +
std::to_string(local_ep.port()) + "/validators";
Env env{
*this,
envconfig([&](std::unique_ptr<Config> cfg) {
cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI);
cfg->section(SECTION_VALIDATOR_LIST_KEYS)
.append(strHex(publisherPublic));
return cfg;
}),
};
env.app().validatorSites().start();
env.app().validatorSites().join();
std::set<PublicKey> startKeys;
for (auto const& val : validators)
startKeys.insert(val.masterPublic);
env.app().validators().onConsensusStart(startKeys);
{
auto const jrr = env.rpc("server_info")[jss::result];
BEAST_EXPECT(jrr[jss::info][jss::validator_list_expires] ==
to_string(expiration));
}
{
auto const jrr = env.rpc("server_state")[jss::result];
BEAST_EXPECT(
jrr[jss::state][jss::validator_list_expires].asUInt() ==
expiration.time_since_epoch().count());
}
{
auto const jrr = env.rpc("validators")[jss::result];
BEAST_EXPECT(jrr[jss::validation_quorum].asUInt() == 2);
BEAST_EXPECT(
jrr[jss::validator_list_expires] == to_string(expiration));
BEAST_EXPECT(jrr[jss::local_static_keys].size() == 0);
BEAST_EXPECT(jrr[jss::trusted_validator_keys].size() ==
expectedKeys.size());
for (auto const& jKey : jrr[jss::trusted_validator_keys])
{
BEAST_EXPECT(expectedKeys.count(jKey.asString()) == 1);
}
if (BEAST_EXPECT(jrr[jss::publisher_lists].size() == 1))
{
auto jp = jrr[jss::publisher_lists][0u];
BEAST_EXPECT(jp[jss::available] == true);
if (BEAST_EXPECT(jp[jss::list].size() == 2))
{
// check entries
std::set<std::string> foundKeys;
for (auto const& k : jp[jss::list])
{
foundKeys.insert(k.asString());
}
BEAST_EXPECT(foundKeys == expectedKeys);
}
BEAST_EXPECT(jp[jss::seq].asUInt() == 1);
BEAST_EXPECT(
jp[jss::pubkey_publisher] == strHex(publisherPublic));
BEAST_EXPECT(jp[jss::expiration] == to_string(expiration));
BEAST_EXPECT(jp[jss::version] == 1);
}
auto jsk = jrr[jss::signing_keys];
BEAST_EXPECT(jsk.size() == 2);
for (auto const& val : validators)
{
BEAST_EXPECT(jsk.isMember(toStr(val.masterPublic)));
BEAST_EXPECT(
jsk[toStr(val.masterPublic)] ==
toStr(val.signingPublic));
}
}
{
auto const jrr = env.rpc("validator_list_sites")[jss::result];
if (BEAST_EXPECT(jrr[jss::validator_sites].size() == 1))
{
auto js = jrr[jss::validator_sites][0u];
BEAST_EXPECT(js[jss::refresh_interval_min].asUInt() == 5);
BEAST_EXPECT(js[jss::uri] == siteURI);
BEAST_EXPECT(js[jss::last_refresh_status] == "accepted");
// The actual time of the update will vary run to run, so
// just verify the time is there
BEAST_EXPECT(js.isMember(jss::last_refresh_time));
}
}
}
}
void
run()
{
testPrivileges();
testStaticUNL();
testDynamicUNL();
}
};
BEAST_DEFINE_TESTSUITE(ValidatorRPC, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -46,3 +46,4 @@
#include <test/rpc/Subscribe_test.cpp>
#include <test/rpc/TransactionEntry_test.cpp>
#include <test/rpc/TransactionHistory_test.cpp>
#include <test/rpc/ValidatorRPC_test.cpp>