From 044dd535131e154e4e5d50df197c7dc3902834e1 Mon Sep 17 00:00:00 2001 From: Brad Chase Date: Thu, 5 Oct 2017 10:52:38 -0400 Subject: [PATCH] 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. --- Builds/VisualStudio2015/RippleD.vcxproj | 14 + .../VisualStudio2015/RippleD.vcxproj.filters | 12 + src/ripple/app/misc/NetworkOPs.cpp | 23 + src/ripple/app/misc/ValidatorList.h | 43 +- src/ripple/app/misc/ValidatorSite.h | 13 + src/ripple/app/misc/impl/ValidatorList.cpp | 146 ++++++- src/ripple/app/misc/impl/ValidatorSite.cpp | 52 ++- src/ripple/protocol/JsonFields.h | 23 +- src/ripple/rpc/handlers/Handlers.h | 3 +- .../rpc/handlers/ValidatorListSites.cpp | 33 ++ src/ripple/rpc/handlers/Validators.cpp | 33 ++ src/ripple/rpc/impl/Handler.cpp | 2 + src/ripple/unity/rpcx2.cpp | 4 + src/test/app/ValidatorList_test.cpp | 121 +++++- src/test/app/ValidatorSite_test.cpp | 208 ++------- src/test/jtx/TrustedPublisherServer.h | 210 +++++++++ src/test/rpc/ValidatorRPC_test.cpp | 400 ++++++++++++++++++ src/test/unity/rpc_test_unity.cpp | 1 + 18 files changed, 1140 insertions(+), 201 deletions(-) create mode 100644 src/ripple/rpc/handlers/ValidatorListSites.cpp create mode 100644 src/ripple/rpc/handlers/Validators.cpp create mode 100644 src/test/jtx/TrustedPublisherServer.h create mode 100644 src/test/rpc/ValidatorRPC_test.cpp diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index d73f5dc2c3..84f0bf2433 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -3025,6 +3025,14 @@ True True + + True + True + + + True + True + @@ -4761,6 +4769,8 @@ + + @@ -5047,6 +5057,10 @@ True True + + True + True + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 326781e37c..7b96923343 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -3630,6 +3630,12 @@ ripple\rpc\handlers + + ripple\rpc\handlers + + + ripple\rpc\handlers + ripple\rpc\handlers @@ -5523,6 +5529,9 @@ test\jtx + + test\jtx + test\jtx @@ -5736,6 +5745,9 @@ test\rpc + + test\rpc + test\server diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 1743034b11..12d5bdb0e2 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -2148,6 +2148,29 @@ Json::Value NetworkOPsImp::getServerInfo (bool human, bool admin) info[jss::validation_quorum] = static_cast( 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(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 ( app_.getIOLatency().count()); diff --git a/src/ripple/app/misc/ValidatorList.h b/src/ripple/app/misc/ValidatorList.h index 985c169e2e..ea7586577e 100644 --- a/src/ripple/app/misc/ValidatorList.h +++ b/src/ripple/app/misc/ValidatorList.h @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -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 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 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 + 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; diff --git a/src/ripple/app/misc/ValidatorSite.h b/src/ripple/app/misc/ValidatorSite.h index 2be33b645b..080e844030 100644 --- a/src/ripple/app/misc/ValidatorSite.h +++ b/src/ripple/app/misc/ValidatorSite.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -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 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 diff --git a/src/ripple/app/misc/impl/ValidatorList.cpp b/src/ripple/app/misc/impl/ValidatorList.cpp index 28c8cefc2f..e87ef02fe8 100644 --- a/src/ripple/app/misc/impl/ValidatorList.cpp +++ b/src/ripple/app/misc/impl/ValidatorList.cpp @@ -21,11 +21,33 @@ #include #include #include +#include #include #include 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 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& publisherList = publisherLists_[pubKey].list; std::vector 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 +ValidatorList::expires() const +{ + boost::shared_lock read_lock{mutex_}; + boost::optional 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 read_lock{mutex_}; + + res[jss::validation_quorum] = static_cast(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(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 func) const diff --git a/src/ripple/app/misc/impl/ValidatorSite.cpp b/src/ripple/app/misc/impl/ValidatorSite.cpp index f092985893..8f7d2f69c5 100644 --- a/src/ripple/app/misc/impl/ValidatorSite.cpp +++ b/src/ripple/app/misc/impl/ValidatorSite.cpp @@ -17,12 +17,13 @@ */ //============================================================================== -#include -#include #include #include +#include +#include #include #include +#include #include #include @@ -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 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 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(site.refreshInterval.count()); + } + } + return jrr; +} } // ripple diff --git a/src/ripple/protocol/JsonFields.h b/src/ripple/protocol/JsonFields.h index 753f78e43d..089d702bc5 100644 --- a/src/ripple/protocol/JsonFields.h +++ b/src/ripple/protocol/JsonFields.h @@ -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 diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index 65b4f841c3..42b1250747 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -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 diff --git a/src/ripple/rpc/handlers/ValidatorListSites.cpp b/src/ripple/rpc/handlers/ValidatorListSites.cpp new file mode 100644 index 0000000000..316bc1683d --- /dev/null +++ b/src/ripple/rpc/handlers/ValidatorListSites.cpp @@ -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 +#include +#include +#include + +namespace ripple { + +Json::Value +doValidatorListSites(RPC::Context& context) +{ + return context.app.validatorSites().getJson(); +} + +} // namespace ripple diff --git a/src/ripple/rpc/handlers/Validators.cpp b/src/ripple/rpc/handlers/Validators.cpp new file mode 100644 index 0000000000..0901063448 --- /dev/null +++ b/src/ripple/rpc/handlers/Validators.cpp @@ -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 +#include +#include +#include + +namespace ripple { + +Json::Value +doValidators(RPC::Context& context) +{ + return context.app.validators().getJson(); +} + +} // namespace ripple diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index 21851610ef..c7d5540e35 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -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 }, diff --git a/src/ripple/unity/rpcx2.cpp b/src/ripple/unity/rpcx2.cpp index 3a1a89c2a9..6f2e940b7e 100644 --- a/src/ripple/unity/rpcx2.cpp +++ b/src/ripple/unity/rpcx2.cpp @@ -48,6 +48,8 @@ #include #include #include +#include +#include #include #include @@ -59,3 +61,5 @@ #include #include #include + + diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index f49279fbd4..650969940f 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -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( + 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( + manifests, manifests, env.app().timeKeeper(), journal); + + std::vector validators = {randomValidator()}; + hash_set 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 cfgPublishers({ + strHex(publisherPublic)}); + PublicKey emptyLocalKey; + std::vector 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 (); } }; diff --git a/src/test/app/ValidatorSite_test.cpp b/src/test/app/ValidatorSite_test.cpp index 3b6c96984c..132552647e 100644 --- a/src/test/app/ValidatorSite_test.cpp +++ b/src/test/app/ValidatorSite_test.cpp @@ -18,7 +18,6 @@ //============================================================================== #include -#include #include #include #include @@ -28,181 +27,18 @@ #include #include #include -#include +#include #include 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; - using resp_type = beast::http::response; - 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 keys, - std::string const& manifest, - int sequence, - std::size_t expiration, - int version, - std::vector 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 diff --git a/src/test/jtx/TrustedPublisherServer.h b/src/test/jtx/TrustedPublisherServer.h new file mode 100644 index 0000000000..4cf67a7219 --- /dev/null +++ b/src/test/jtx/TrustedPublisherServer.h @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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; + using resp_type = beast::http::response; + 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 keys, + std::string const& manifest, + int sequence, + NetClock::time_point expiration, + int version, + std::vector 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 diff --git a/src/test/rpc/ValidatorRPC_test.cpp b/src/test/rpc/ValidatorRPC_test.cpp new file mode 100644 index 0000000000..87ea06bf31 --- /dev/null +++ b/src/test/rpc/ValidatorRPC_test.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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(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 const keys = { + "n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7", + "n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj"}; + Env env{ + *this, + envconfig([&keys](std::unique_ptr 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 validators = {randomValidator(), randomValidator()}; + std::set 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 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::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 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 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 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 diff --git a/src/test/unity/rpc_test_unity.cpp b/src/test/unity/rpc_test_unity.cpp index 3431d8bc22..8ebbf2d9eb 100644 --- a/src/test/unity/rpc_test_unity.cpp +++ b/src/test/unity/rpc_test_unity.cpp @@ -46,3 +46,4 @@ #include #include #include +#include