From b45f45dcef8f15b43d04b474c736466a8b60df53 Mon Sep 17 00:00:00 2001 From: wilsonianb Date: Wed, 2 Nov 2016 16:14:31 -0700 Subject: [PATCH] Fetch validator lists from remote sites: Validator lists from configured remote sites are fetched at a regular interval. Fetched lists are expected to be in JSON format and contain the following fields: * "manifest": Base64-encoded serialization of a manifest containing the validator publisher's master and signing public keys. * "blob": Base64-encoded JSON string containing a "sequence", "expiration" and "validators" field. "expiration" contains the Ripple timestamp (seconds since January 1st, 2000 (00:00 UTC)) for when the list expires. "validators" contains an array of objects with a "validation_public_key" field. * "signature": Hex-encoded signature of the blob using the publisher's signing key. * "version": 1 * "refreshInterval" (optional) --- Builds/VisualStudio2015/RippleD.vcxproj | 18 + .../VisualStudio2015/RippleD.vcxproj.filters | 24 ++ doc/rippled-example.cfg | 10 +- doc/validators-example.txt | 11 +- src/ripple/app/main/Application.cpp | 22 + src/ripple/app/main/Application.h | 2 + src/ripple/app/misc/ValidatorSite.h | 169 ++++++++ src/ripple/app/misc/detail/Work.h | 47 +++ src/ripple/app/misc/detail/WorkBase.h | 222 ++++++++++ src/ripple/app/misc/detail/WorkPlain.h | 76 ++++ src/ripple/app/misc/detail/WorkSSL.h | 137 ++++++ src/ripple/app/misc/impl/ValidatorSite.cpp | 280 +++++++++++++ src/ripple/core/ConfigSections.h | 1 + src/ripple/core/impl/Config.cpp | 15 + src/ripple/unity/app_misc.cpp | 1 + src/test/app/ValidatorSite_test.cpp | 392 ++++++++++++++++++ src/test/core/Config_test.cpp | 53 ++- src/test/unity/app_test_unity.cpp | 1 + 18 files changed, 1474 insertions(+), 7 deletions(-) create mode 100644 src/ripple/app/misc/ValidatorSite.h create mode 100644 src/ripple/app/misc/detail/Work.h create mode 100644 src/ripple/app/misc/detail/WorkBase.h create mode 100644 src/ripple/app/misc/detail/WorkPlain.h create mode 100644 src/ripple/app/misc/detail/WorkSSL.h create mode 100644 src/ripple/app/misc/impl/ValidatorSite.cpp create mode 100644 src/test/app/ValidatorSite_test.cpp diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index 6c57577cf3..79b3d3493a 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -1035,6 +1035,14 @@ + + + + + + + + @@ -1077,6 +1085,10 @@ True True + + True + True + @@ -1109,6 +1121,8 @@ + + True True @@ -4275,6 +4289,10 @@ True True + + True + True + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 2c99d99fb4..d44b329bae 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -121,6 +121,9 @@ {5A1509B2-871B-A7AC-1E60-544D3F398741} + + {2919FCCC-A707-22B8-FFB4-89494A8AC070} + {C4BDB9F8-7DB7-E304-D286-098085D5D16E} @@ -1545,6 +1548,18 @@ ripple\app\misc + + ripple\app\misc\detail + + + ripple\app\misc\detail + + + ripple\app\misc\detail + + + ripple\app\misc\detail + ripple\app\misc @@ -1581,6 +1596,9 @@ ripple\app\misc\impl + + ripple\app\misc\impl + ripple\app\misc @@ -1620,6 +1638,9 @@ ripple\app\misc + + ripple\app\misc + ripple\app\paths @@ -5040,6 +5061,9 @@ test\app + + test\app + test\basics diff --git a/doc/rippled-example.cfg b/doc/rippled-example.cfg index 552daf1c45..a94ea9afe3 100644 --- a/doc/rippled-example.cfg +++ b/doc/rippled-example.cfg @@ -607,13 +607,15 @@ # needed to accept consensus. # # The contents of the file should include a [validators] and/or -# [validator_list_keys] entries. +# [validator_list_sites] and [validator_list_keys] entries. # [validators] should be followed by a list of validation public keys of # nodes, one per line. +# [validator_list_sites] should be followed by a list of URIs each serving a +# list of recommended validators. # [validator_list_keys] should be followed by a list of keys belonging to -# trusted validator list publishers. Validator lists will only be -# considered if the list is accompanied by a valid signature from a trusted -# publisher key. +# trusted validator list publishers. Validator lists fetched from configured +# sites will only be considered if the list is accompanied by a valid +# signature from a trusted publisher key. # # Specify the file by its name or path. # Unless an absolute path is specified, it will be considered relative to diff --git a/doc/validators-example.txt b/doc/validators-example.txt index 885b3a5363..57f2f9a9e1 100644 --- a/doc/validators-example.txt +++ b/doc/validators-example.txt @@ -23,13 +23,20 @@ # n9KorY8QtTdRx7TVDpwnG9NvyxsDwHUKUEeDLY3AkiGncVaSXZi5 # n9MqiExBcoG19UXwoLjBJnhsxEhAZMuWwJDRdkyDz1EkEkwzQTNt # +# [validator_list_sites] # +# List of URIs serving lists of recommended validators. +# +# Examples: +# https://ripple.com/validators +# http://127.0.0.1:8000 # # [validator_list_keys] # # List of keys belonging to trusted validator list publishers. -# Validator lists will only be considered if the list is accompanied by a -# valid signature from a trusted publisher key. +# Validator lists fetched from configured sites will only be considered +# if the list is accompanied by a valid signature from a trusted +# publisher key. # Validator list keys should be hex-encoded. # # Examples: diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 1ebd5d3634..0b9c618e17 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -351,6 +352,7 @@ public: std::unique_ptr validatorManifests_; std::unique_ptr publisherManifests_; std::unique_ptr validators_; + std::unique_ptr validatorSites_; std::unique_ptr serverHandler_; std::unique_ptr m_amendmentTable; std::unique_ptr mFeeTrack; @@ -486,6 +488,9 @@ public: *validatorManifests_, *publisherManifests_, *timeKeeper_, logs_->journal("ValidatorList"), config_->VALIDATION_QUORUM)) + , validatorSites_ (std::make_unique ( + get_io_service (), *validators_, logs_->journal("ValidatorSite"))) + , serverHandler_ (make_ServerHandler (*this, *m_networkOPs, get_io_service (), *m_jobQueue, *m_networkOPs, *m_resourceManager, *m_collectorManager)) @@ -701,6 +706,11 @@ public: return *validators_; } + ValidatorSite& validatorSites () override + { + return *validatorSites_; + } + ManifestCache& validatorManifests() override { return *validatorManifests_; @@ -866,6 +876,8 @@ public: mValidations->flush (); + validatorSites_->stop (); + // TODO Store manifests in manifests.sqlite instead of wallet.db validatorManifests_->save (getWalletDB (), "ValidatorManifests", [this](PublicKey const& pubKey) @@ -1112,6 +1124,14 @@ bool ApplicationImp::setup() return false; } + if (!validatorSites_->load ( + config().section (SECTION_VALIDATOR_LIST_SITES).values ())) + { + JLOG(m_journal.fatal()) << + "Invalid entry in [" << SECTION_VALIDATOR_LIST_SITES << "]"; + return false; + } + m_nodeStore->tune (config_->getSize (siNodeCacheSize), config_->getSize (siNodeCacheAge)); m_ledgerMaster->tune (config_->getSize (siLedgerSize), config_->getSize (siLedgerAge)); family().treecache().setTargetSize (config_->getSize (siTreeCacheSize)); @@ -1133,6 +1153,8 @@ bool ApplicationImp::setup() *config_); add (*m_overlay); // add to PropertyStream + validatorSites_->start (); + // start first consensus round if (! m_networkOPs->beginConsensus(m_ledgerMaster->getClosedLedger()->info().hash)) { diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index 8f6d4ee5a1..567adfbca9 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -64,6 +64,7 @@ class TransactionMaster; class TxQ; class Validations; class ValidatorList; +class ValidatorSite; class Cluster; class DatabaseCon; @@ -121,6 +122,7 @@ public: virtual Overlay& overlay () = 0; virtual TxQ& getTxQ() = 0; virtual ValidatorList& validators () = 0; + virtual ValidatorSite& validatorSites () = 0; virtual ManifestCache& validatorManifests () = 0; virtual ManifestCache& publisherManifests () = 0; virtual Cluster& cluster () = 0; diff --git a/src/ripple/app/misc/ValidatorSite.h b/src/ripple/app/misc/ValidatorSite.h new file mode 100644 index 0000000000..5fba458e03 --- /dev/null +++ b/src/ripple/app/misc/ValidatorSite.h @@ -0,0 +1,169 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 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. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_VALIDATORSITE_H_INCLUDED +#define RIPPLE_APP_MISC_VALIDATORSITE_H_INCLUDED + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +/** + Validator Sites + --------------- + + This class manages the set of configured remote sites used to fetch the + latest published recommended validator lists. + + Lists are fetched at a regular interval. + Fetched lists are expected to be in JSON format and contain the following + fields: + + @li @c "blob": Base64-encoded JSON string containing a @c "sequence", @c + "expiration", and @c "validators" field. @c "expiration" contains the + Ripple timestamp (seconds since January 1st, 2000 (00:00 UTC)) for when + the list expires. @c "validators" contains an array of objects with a + @c "validation_public_key" field. + @c "validation_public_key" should be the hex-encoded master public key. + + @li @c "manifest": Base64-encoded serialization of a manifest containing the + publisher's master and signing public keys. + + @li @c "signature": Hex-encoded signature of the blob using the publisher's + signing key. + + @li @c "version": 1 + + @li @c "refreshInterval" (optional) +*/ +class ValidatorSite +{ + friend class Work; + +private: + using error_code = boost::system::error_code; + using clock_type = std::chrono::system_clock; + + struct Site + { + std::string uri; + parsedURL pUrl; + std::chrono::minutes refreshInterval; + clock_type::time_point nextRefresh; + }; + + boost::asio::io_service& ios_; + ValidatorList& validators_; + beast::Journal j_; + std::mutex mutable sites_mutex_; + std::mutex mutable state_mutex_; + + std::condition_variable cv_; + std::weak_ptr work_; + boost::asio::basic_waitable_timer timer_; + + // A list is currently being fetched from a site + std::atomic fetching_; + + // One or more lists are due to be fetched + std::atomic pending_; + std::atomic stopping_; + + // The configured list of URIs for fetching lists + std::vector sites_; + +public: + ValidatorSite ( + boost::asio::io_service& ios, + ValidatorList& validators, + beast::Journal j); + ~ValidatorSite (); + + /** Load configured site URIs. + + @param siteURIs List of URIs to fetch published validator lists + + @par Thread Safety + + May be called concurrently + + @return `false` if an entry is invalid or unparsable + */ + bool + load ( + std::vector const& siteURIs); + + /** Start fetching lists from sites + + This does nothing if list fetching has already started + + @par Thread Safety + + May be called concurrently + */ + void + start (); + + /** Wait for current fetches from sites to complete + + @par Thread Safety + + May be called concurrently + */ + void + join (); + + /** Stop fetching lists from sites + + This blocks until list fetching has stopped + + @par Thread Safety + + May be called concurrently + */ + void + stop (); + +private: + /// Queue next site to be fetched + void + setTimer (); + + /// Fetch site whose time has come + void + onTimer ( + std::size_t siteIdx, + error_code const& ec); + + /// Store latest list fetched from site + void + onSiteFetch ( + boost::system::error_code const& ec, + detail::response_type&& res, + std::size_t siteIdx); +}; + +} // ripple + +#endif diff --git a/src/ripple/app/misc/detail/Work.h b/src/ripple/app/misc/detail/Work.h new file mode 100644 index 0000000000..b3be25c18d --- /dev/null +++ b/src/ripple/app/misc/detail/Work.h @@ -0,0 +1,47 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 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. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_DETAIL_WORK_H_INCLUDED +#define RIPPLE_APP_MISC_DETAIL_WORK_H_INCLUDED + +#include +#include + +namespace ripple { + +namespace detail { + +using response_type = + beast::http::response; + +class Work +{ +public: + virtual ~Work() = default; + + virtual void run() = 0; + + virtual void cancel() = 0; +}; + +} // detail + +} // ripple + +#endif diff --git a/src/ripple/app/misc/detail/WorkBase.h b/src/ripple/app/misc/detail/WorkBase.h new file mode 100644 index 0000000000..49cfcd45a7 --- /dev/null +++ b/src/ripple/app/misc/detail/WorkBase.h @@ -0,0 +1,222 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 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. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_DETAIL_WORKBASE_H_INCLUDED +#define RIPPLE_APP_MISC_DETAIL_WORKBASE_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +namespace detail { + +template +class WorkBase + : public Work +{ +protected: + using error_code = boost::system::error_code; + +public: + using callback_type = + std::function; +protected: + using socket_type = boost::asio::ip::tcp::socket; + using endpoint_type = boost::asio::ip::tcp::endpoint; + using resolver_type = boost::asio::ip::tcp::resolver; + using query_type = resolver_type::query; + using request_type = + beast::http::request; + + std::string host_; + std::string path_; + std::string port_; + callback_type cb_; + boost::asio::io_service& ios_; + boost::asio::io_service::strand strand_; + resolver_type resolver_; + socket_type socket_; + request_type req_; + response_type res_; + beast::streambuf read_buf_; + +public: + WorkBase( + std::string const& host, std::string const& path, + std::string const& port, + boost::asio::io_service& ios, callback_type cb); + ~WorkBase(); + + Impl& + impl() + { + return *static_cast(this); + } + + void run() override; + + void cancel() override; + + void + fail(error_code const& ec); + + void + onResolve(error_code const& ec, resolver_type::iterator it); + + void + onStart(); + + void + onRequest(error_code const& ec); + + void + onResponse(error_code const& ec); +}; + +//------------------------------------------------------------------------------ + +template +WorkBase::WorkBase(std::string const& host, + std::string const& path, std::string const& port, + boost::asio::io_service& ios, callback_type cb) + : host_(host) + , path_(path) + , port_(port) + , cb_(std::move(cb)) + , ios_(ios) + , strand_(ios) + , resolver_(ios) + , socket_(ios) +{ +} + +template +WorkBase::~WorkBase() +{ + if (cb_) + cb_ (make_error_code(boost::system::errc::not_a_socket), + std::move(res_)); +} + +template +void +WorkBase::run() +{ + if (! strand_.running_in_this_thread()) + return ios_.post(strand_.wrap (std::bind( + &WorkBase::run, impl().shared_from_this()))); + + resolver_.async_resolve( + query_type{host_, port_}, + strand_.wrap (std::bind(&WorkBase::onResolve, impl().shared_from_this(), + beast::asio::placeholders::error, + beast::asio::placeholders::iterator))); +} + +template +void +WorkBase::cancel() +{ + if (! strand_.running_in_this_thread()) + { + return ios_.post(strand_.wrap (std::bind( + &WorkBase::cancel, impl().shared_from_this()))); + } + + error_code ec; + resolver_.cancel(); + socket_.cancel (ec); +} + +template +void +WorkBase::fail(error_code const& ec) +{ + if (cb_) + { + cb_(ec, std::move(res_)); + cb_ = nullptr; + } +} + +template +void +WorkBase::onResolve(error_code const& ec, resolver_type::iterator it) +{ + if (ec) + return fail(ec); + + socket_.async_connect(*it, + strand_.wrap (std::bind(&Impl::onConnect, impl().shared_from_this(), + beast::asio::placeholders::error))); +} + +template +void +WorkBase::onStart() +{ + req_.method = "GET"; + req_.url = path_.empty() ? "/" : path_; + req_.version = 11; + req_.fields.replace ( + "Host", host_ + ":" + port_); + req_.fields.replace ("User-Agent", BuildInfo::getFullVersionString()); + beast::http::prepare (req_); + + beast::http::async_write(impl().stream(), req_, + strand_.wrap (std::bind (&WorkBase::onRequest, + impl().shared_from_this(), beast::asio::placeholders::error))); +} + +template +void +WorkBase::onRequest(error_code const& ec) +{ + if (ec) + return fail(ec); + + beast::http::async_read (impl().stream(), read_buf_, res_, + strand_.wrap (std::bind (&WorkBase::onResponse, + impl().shared_from_this(), beast::asio::placeholders::error))); +} + +template +void +WorkBase::onResponse(error_code const& ec) +{ + if (ec) + return fail(ec); + + assert(cb_); + cb_(ec, std::move(res_)); + cb_ = nullptr; +} + +} // detail + +} // ripple + +#endif diff --git a/src/ripple/app/misc/detail/WorkPlain.h b/src/ripple/app/misc/detail/WorkPlain.h new file mode 100644 index 0000000000..6942e823e7 --- /dev/null +++ b/src/ripple/app/misc/detail/WorkPlain.h @@ -0,0 +1,76 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 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. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_DETAIL_WORKPLAIN_H_INCLUDED +#define RIPPLE_APP_MISC_DETAIL_WORKPLAIN_H_INCLUDED + +#include + +namespace ripple { + +namespace detail { + +// Work over TCP/IP +class WorkPlain : public WorkBase + , public std::enable_shared_from_this +{ + friend class WorkBase; + +public: + WorkPlain( + std::string const& host, + std::string const& path, std::string const& port, + boost::asio::io_service& ios, callback_type cb); + ~WorkPlain() = default; + +private: + void + onConnect(error_code const& ec); + + socket_type& + stream() + { + return socket_; + } +}; + +//------------------------------------------------------------------------------ + +WorkPlain::WorkPlain( + std::string const& host, + std::string const& path, std::string const& port, + boost::asio::io_service& ios, callback_type cb) + : WorkBase (host, path, port, ios, cb) +{ +} + +void +WorkPlain::onConnect(error_code const& ec) +{ + if (ec) + return fail(ec); + + onStart (); +} + +} // detail + +} // ripple + +#endif diff --git a/src/ripple/app/misc/detail/WorkSSL.h b/src/ripple/app/misc/detail/WorkSSL.h new file mode 100644 index 0000000000..363fab0113 --- /dev/null +++ b/src/ripple/app/misc/detail/WorkSSL.h @@ -0,0 +1,137 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 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. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_DETAIL_WORKSSL_H_INCLUDED +#define RIPPLE_APP_MISC_DETAIL_WORKSSL_H_INCLUDED + +#include +#include +#include +#include +#include + +namespace ripple { + +namespace detail { + +class SSLContext : public boost::asio::ssl::context +{ +public: + SSLContext() + : boost::asio::ssl::context(boost::asio::ssl::context::sslv23) + { + boost::system::error_code ec; + set_default_verify_paths (ec); + + if (ec) + { + Throw ( + boost::str (boost::format ( + "Failed to set_default_verify_paths: %s") % + ec.message ())); + } + } +}; + +// Work over SSL +class WorkSSL : public WorkBase + , public std::enable_shared_from_this +{ + friend class WorkBase; + +private: + using stream_type = boost::asio::ssl::stream; + + SSLContext context_; + stream_type stream_; + +public: + WorkSSL( + std::string const& host, + std::string const& path, std::string const& port, + boost::asio::io_service& ios, callback_type cb); + ~WorkSSL() = default; + +private: + stream_type& + stream() + { + return stream_; + } + + void + onConnect(error_code const& ec); + + void + onHandshake(error_code const& ec); + + static bool + rfc2818_verify ( + std::string const& domain, + bool preverified, + boost::asio::ssl::verify_context& ctx) + { + return + boost::asio::ssl::rfc2818_verification (domain) (preverified, ctx); + } +}; + +//------------------------------------------------------------------------------ + +WorkSSL::WorkSSL( + std::string const& host, + std::string const& path, std::string const& port, + boost::asio::io_service& ios, callback_type cb) + : WorkBase (host, path, port, ios, cb) + , context_() + , stream_ (socket_, context_) +{ + stream_.set_verify_mode (boost::asio::ssl::verify_peer); + stream_.set_verify_callback ( + std::bind ( + &WorkSSL::rfc2818_verify, host_, + std::placeholders::_1, std::placeholders::_2)); +} + +void +WorkSSL::onConnect(error_code const& ec) +{ + if (ec) + return fail(ec); + + stream_.async_handshake( + boost::asio::ssl::stream_base::client, + strand_.wrap (boost::bind(&WorkSSL::onHandshake, shared_from_this(), + boost::asio::placeholders::error))); +} + +void +WorkSSL::onHandshake(error_code const& ec) +{ + if (ec) + return fail(ec); + + onStart (); +} + +} // detail + +} // ripple + +#endif diff --git a/src/ripple/app/misc/impl/ValidatorSite.cpp b/src/ripple/app/misc/impl/ValidatorSite.cpp new file mode 100644 index 0000000000..90cd60655d --- /dev/null +++ b/src/ripple/app/misc/impl/ValidatorSite.cpp @@ -0,0 +1,280 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 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 + +namespace ripple { + +// default site query frequency - 5 minutes +auto constexpr DEFAULT_REFRESH_INTERVAL = std::chrono::minutes{5}; + +ValidatorSite::ValidatorSite ( + boost::asio::io_service& ios, + ValidatorList& validators, + beast::Journal j) + : ios_ (ios) + , validators_ (validators) + , j_ (j) + , timer_ (ios_) + , fetching_ (false) + , pending_ (false) + , stopping_ (false) +{ +} + +ValidatorSite::~ValidatorSite() +{ + std::unique_lock lock{state_mutex_}; + if (timer_.expires_at().time_since_epoch().count()) + { + if (! stopping_) + { + lock.unlock(); + stop(); + } + else + { + cv_.wait(lock, [&]{ return ! fetching_; }); + } + } +} + +bool +ValidatorSite::load ( + std::vector const& siteURIs) +{ + JLOG (j_.debug()) << + "Loading configured validator list sites"; + + std::lock_guard lock{sites_mutex_}; + + for (auto uri : siteURIs) + { + parsedURL pUrl; + if (! parseUrl (pUrl, uri) || + (pUrl.scheme != "http" && pUrl.scheme != "https")) + { + JLOG (j_.error()) << + "Invalid validator site uri: " << uri; + return false; + } + + if (! pUrl.port) + pUrl.port = (pUrl.scheme == "https") ? 443 : 80; + + sites_.push_back ({ + uri, pUrl, DEFAULT_REFRESH_INTERVAL, clock_type::now()}); + } + + JLOG (j_.debug()) << + "Loaded " << siteURIs.size() << " sites"; + + return true; +} + +void +ValidatorSite::start () +{ + std::lock_guard lock{state_mutex_}; + if (! timer_.expires_at().time_since_epoch().count()) + setTimer (); +} + +void +ValidatorSite::join () +{ + std::unique_lock lock{state_mutex_}; + cv_.wait(lock, [&]{ return ! pending_; }); +} + +void +ValidatorSite::stop() +{ + std::unique_lock lock{state_mutex_}; + stopping_ = true; + cv_.wait(lock, [&]{ return ! fetching_; }); + + if(auto sp = work_.lock()) + sp->cancel(); + + error_code ec; + timer_.cancel(ec); + stopping_ = false; + pending_ = false; + cv_.notify_all(); +} + +void +ValidatorSite::setTimer () +{ + std::lock_guard lock{sites_mutex_}; + auto next = sites_.end(); + + for (auto it = sites_.begin (); it != sites_.end (); ++it) + if (next == sites_.end () || it->nextRefresh < next->nextRefresh) + next = it; + + if (next != sites_.end ()) + { + pending_ = next->nextRefresh <= clock_type::now(); + cv_.notify_all(); + timer_.expires_at (next->nextRefresh); + timer_.async_wait (std::bind (&ValidatorSite::onTimer, this, + std::distance (sites_.begin (), next), + beast::asio::placeholders::error)); + } +} + +void +ValidatorSite::onTimer ( + std::size_t siteIdx, + error_code const& ec) +{ + if (ec == boost::asio::error::operation_aborted) + return; + if (ec) + { + JLOG(j_.error()) << + "ValidatorSite::onTimer: " << ec.message(); + return; + } + + std::lock_guard lock{sites_mutex_}; + sites_[siteIdx].nextRefresh = + clock_type::now() + DEFAULT_REFRESH_INTERVAL; + + assert(! fetching_); + fetching_ = true; + + std::shared_ptr sp; + if (sites_[siteIdx].pUrl.scheme == "https") + { + sp = std::make_shared( + sites_[siteIdx].pUrl.domain, + sites_[siteIdx].pUrl.path, + std::to_string(*sites_[siteIdx].pUrl.port), + ios_, + [this, siteIdx](error_code const& err, detail::response_type&& resp) + { + onSiteFetch (err, std::move(resp), siteIdx); + }); + } + else + { + sp = std::make_shared( + sites_[siteIdx].pUrl.domain, + sites_[siteIdx].pUrl.path, + std::to_string(*sites_[siteIdx].pUrl.port), + ios_, + [this, siteIdx](error_code const& err, detail::response_type&& resp) + { + onSiteFetch (err, std::move(resp), siteIdx); + }); + } + + work_ = sp; + sp->run (); +} + +void +ValidatorSite::onSiteFetch( + boost::system::error_code const& ec, + detail::response_type&& res, + std::size_t siteIdx) +{ + if (! ec && res.status != 200) + { + std::lock_guard lock{sites_mutex_}; + JLOG (j_.warn()) << + "Request for validator list at " << + sites_[siteIdx].uri << " returned " << res.status; + } + else if (! ec) + { + std::lock_guard lock{sites_mutex_}; + Json::Reader r; + Json::Value body; + if (r.parse(res.body.data(), body) && + body.isObject () && + body.isMember("blob") && body["blob"].isString () && + body.isMember("manifest") && body["manifest"].isString () && + body.isMember("signature") && body["signature"].isString() && + body.isMember("version") && body["version"].isInt()) + { + + auto const disp = validators_.applyList ( + body["manifest"].asString (), + body["blob"].asString (), + body["signature"].asString(), + body["version"].asUInt()); + + if (ListDisposition::accepted == disp) + { + JLOG (j_.debug()) << + "Applied new validator list from " << + sites_[siteIdx].uri; + } + else if (ListDisposition::stale == disp) + { + JLOG (j_.warn()) << + "Stale validator list from " << sites_[siteIdx].uri; + } + else if (ListDisposition::untrusted == disp) + { + JLOG (j_.warn()) << + "Untrusted validator list from " << + sites_[siteIdx].uri; + } + else if (ListDisposition::invalid == disp) + { + JLOG (j_.warn()) << + "Invalid validator list from " << + sites_[siteIdx].uri; + } + + if (body.isMember ("refresh_interval") && + body["refresh_interval"].isNumeric ()) + { + sites_[siteIdx].refreshInterval = + std::chrono::minutes{body["refresh_interval"].asUInt ()}; + } + } + else + { + JLOG (j_.warn()) << + "Unable to parse JSON response from " << + sites_[siteIdx].uri; + } + } + + std::lock_guard lock{state_mutex_}; + fetching_ = false; + if (! stopping_) + setTimer (); + cv_.notify_all(); +} + +} // ripple diff --git a/src/ripple/core/ConfigSections.h b/src/ripple/core/ConfigSections.h index 95108ced7b..d85a0e2983 100644 --- a/src/ripple/core/ConfigSections.h +++ b/src/ripple/core/ConfigSections.h @@ -64,6 +64,7 @@ struct ConfigSection #define SECTION_WEBSOCKET_PING_FREQ "websocket_ping_frequency" #define SECTION_VALIDATOR_KEYS "validator_keys" #define SECTION_VALIDATOR_LIST_KEYS "validator_list_keys" +#define SECTION_VALIDATOR_LIST_SITES "validator_list_sites" #define SECTION_VALIDATORS "validators" #define SECTION_VALIDATION_MANIFEST "validation_manifest" #define SECTION_VETO_AMENDMENTS "veto_amendments" diff --git a/src/ripple/core/impl/Config.cpp b/src/ripple/core/impl/Config.cpp index 60ed2954f3..5b24b2c746 100644 --- a/src/ripple/core/impl/Config.cpp +++ b/src/ripple/core/impl/Config.cpp @@ -503,6 +503,13 @@ void Config::loadFromString (std::string const& fileContents) if (valKeyEntries) section (SECTION_VALIDATOR_KEYS).append (*valKeyEntries); + auto valSiteEntries = getIniFileSection( + iniFile, + SECTION_VALIDATOR_LIST_SITES); + + if (valSiteEntries) + section (SECTION_VALIDATOR_LIST_SITES).append (*valSiteEntries); + auto valListKeys = getIniFileSection( iniFile, SECTION_VALIDATOR_LIST_KEYS); @@ -523,6 +530,14 @@ void Config::loadFromString (std::string const& fileContents) // Consolidate [validator_keys] and [validators] section (SECTION_VALIDATORS).append ( section (SECTION_VALIDATOR_KEYS).lines ()); + + if (! section (SECTION_VALIDATOR_LIST_SITES).lines().empty() && + section (SECTION_VALIDATOR_LIST_KEYS).lines().empty()) + { + Throw ( + "[" + std::string(SECTION_VALIDATOR_LIST_KEYS) + + "] config section is missing"); + } } { diff --git a/src/ripple/unity/app_misc.cpp b/src/ripple/unity/app_misc.cpp index 33ad6ca287..ae6522f2d9 100644 --- a/src/ripple/unity/app_misc.cpp +++ b/src/ripple/unity/app_misc.cpp @@ -33,3 +33,4 @@ #include #include #include +#include diff --git a/src/test/app/ValidatorSite_test.cpp b/src/test/app/ValidatorSite_test.cpp new file mode 100644 index 0000000000..7a96691ae3 --- /dev/null +++ b/src/test/app/ValidatorSite_test.cpp @@ -0,0 +1,392 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright 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 +#include +#include + +namespace ripple { +namespace test { + +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) + "\"},"; + } + 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, + beast::asio::placeholders::error)); + } + + ~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) + { + if(! acceptor_.is_open()) + return; + if(ec) + 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, + beast::asio::placeholders::error)); + } + + void + do_peer(int id, socket_type&& sock0) + { + socket_type sock(std::move(sock0)); + beast::streambuf sb; + error_code ec; + for(;;) + { + req_type req; + beast::http::read(sock, sb, req, ec); + if(ec) + break; + auto path = req.url; + if(path != "/validators") + { + resp_type res; + res.status = 404; + res.reason = "Not Found"; + res.version = req.version; + res.fields.insert("Server", "http_sync_server"); + res.fields.insert("Content-Type", "text/html"); + res.body = "The file '" + path + "' was not found"; + prepare(res); + write(sock, res, ec); + if(ec) + break; + } + resp_type res; + res.status = 200; + res.reason = "OK"; + res.version = req.version; + res.fields.insert("Server", "http_sync_server"); + res.fields.insert("Content-Type", "application/json"); + + res.body = list_; + try + { + prepare(res); + } + catch(std::exception const& e) + { + res = {}; + res.status = 500; + res.reason = "Internal Error"; + res.version = req.version; + res.fields.insert("Server", "http_sync_server"); + res.fields.insert("Content-Type", "text/html"); + res.body = + std::string{"An internal error occurred"} + e.what(); + prepare(res); + } + write(sock, res, ec); + if(ec) + break; + } + } +}; + +class ValidatorSite_test : public beast::unit_test::suite +{ +private: + static + PublicKey + randomNode () + { + return derivePublicKey (KeyType::secp256k1, randomSecretKey()); + } + + 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())); + } + + void + testConfigLoad () + { + testcase ("Config Load"); + + using namespace jtx; + + Env env (*this); + auto trustedSites = std::make_unique ( + env.app().getIOService(), env.app().validators(), beast::Journal()); + + // load should accept empty sites list + std::vector emptyCfgSites; + BEAST_EXPECT(trustedSites->load (emptyCfgSites)); + + // load should accept valid validator site uris + std::vector cfgSites({ + "http://ripple.com/", + "http://ripple.com/validators", + "http://ripple.com:8080/validators", + "http://207.261.33.37/validators", + "http://207.261.33.37:8080/validators", + "https://ripple.com/validators", + "https://ripple.com:443/validators"}); + BEAST_EXPECT(trustedSites->load (cfgSites)); + + // load should reject validator site uris with invalid schemes + std::vector badSites( + {"ftp://ripple.com/validators"}); + BEAST_EXPECT(!trustedSites->load (badSites)); + + badSites[0] = "wss://ripple.com/validators"; + BEAST_EXPECT(!trustedSites->load (badSites)); + + badSites[0] = "ripple.com/validators"; + BEAST_EXPECT(!trustedSites->load (badSites)); + } + + void + testFetchList () + { + testcase ("Fetch list"); + + using namespace jtx; + + Env env (*this); + auto& ioService = env.app ().getIOService (); + auto& trustedKeys = env.app ().validators (); + + beast::Journal journal; + + PublicKey emptyLocalKey; + std::vector emptyCfgKeys; + + auto const publisherSecret1 = randomSecretKey(); + auto const publisherPublic1 = + derivePublicKey(KeyType::ed25519, publisherSecret1); + auto const pubSigningKeys1 = randomKeyPair(KeyType::secp256k1); + + auto const manifest1 = makeManifestString ( + publisherPublic1, publisherSecret1, + pubSigningKeys1.first, pubSigningKeys1.second, 1); + + auto const publisherSecret2 = randomSecretKey(); + auto const publisherPublic2 = + derivePublicKey(KeyType::ed25519, publisherSecret2); + auto const pubSigningKeys2 = randomKeyPair(KeyType::secp256k1); + + auto const manifest2 = makeManifestString ( + publisherPublic2, publisherSecret2, + pubSigningKeys2.first, pubSigningKeys2.second, 1); + + std::vector cfgPublishers({ + strHex(publisherPublic1), + strHex(publisherPublic2)}); + + BEAST_EXPECT(trustedKeys.load ( + emptyLocalKey, emptyCfgKeys, cfgPublishers)); + + auto constexpr listSize = 20; + std::vector list1; + list1.reserve (listSize); + while (list1.size () < listSize) + list1.push_back (randomNode()); + + std::vector list2; + list2.reserve (listSize); + while (list2.size () < listSize) + list2.push_back (randomNode()); + + 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}; + + 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); + + http_sync_server server2( + ep2, ioService, pubSigningKeys2, manifest2, sequence, + expiration.time_since_epoch().count(), version, list2); + + { + // fetch single site + std::vector cfgSites( + {"http://127.0.0.1:" + std::to_string(port1) + "/validators"}); + + auto sites = std::make_unique ( + env.app().getIOService(), env.app().validators(), journal); + + sites->load (cfgSites); + sites->start(); + sites->join(); + + for (auto const& val : list1) + BEAST_EXPECT(trustedKeys.listed (val)); + } + { + // fetch multiple sites + std::vector cfgSites({ + "http://127.0.0.1:" + std::to_string(port1) + "/validators", + "http://127.0.0.1:" + std::to_string(port2) + "/validators"}); + + auto sites = std::make_unique ( + env.app().getIOService(), env.app().validators(), journal); + + sites->load (cfgSites); + sites->start(); + sites->join(); + + for (auto const& val : list1) + BEAST_EXPECT(trustedKeys.listed (val)); + + for (auto const& val : list2) + BEAST_EXPECT(trustedKeys.listed (val)); + } + } + +public: + void + run() override + { + testConfigLoad (); + testFetchList (); + } +}; + +BEAST_DEFINE_TESTSUITE(ValidatorSite, app, ripple); + +} // test +} // ripple diff --git a/src/test/core/Config_test.cpp b/src/test/core/Config_test.cpp index f852d52f37..759b740ac0 100644 --- a/src/test/core/Config_test.cpp +++ b/src/test/core/Config_test.cpp @@ -300,6 +300,10 @@ nHUhG1PgAG8H8myUENypM35JgfqXAKNQvRVVAFDRzJrny5eZN8d5 nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 nHUPDdcdb2Y5DZAJne4c2iabFuAP3F34xZUgYQT2NH7qfkdapgnz +[validator_list_sites] +recommendedripplevalidators.com +moreripplevalidators.net + [validator_list_keys] 03E74EE14CB525AFBB9F1B7D86CD58ECC4B91452294B42AB4E78F260BD905C091D 030775A669685BD6ABCEBD80385921C7851783D991A8055FD21D2F3966C96F1B56 @@ -527,19 +531,50 @@ nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 5); } { - // load validator list keys from config + // load validator list sites and keys from config Config c; std::string toLoad(R"rippleConfig( +[validator_list_sites] +ripplevalidators.com +trustthesevalidators.gov + [validator_list_keys] 021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566 )rippleConfig"); c.loadFromString (toLoad); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_SITES).values ().size () == 2); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_SITES).values ()[0] == + "ripplevalidators.com"); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_SITES).values ()[1] == + "trustthesevalidators.gov"); BEAST_EXPECT( c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 1); BEAST_EXPECT( c.section (SECTION_VALIDATOR_LIST_KEYS).values ()[0] == "021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566"); } + { + // load should throw if [validator_list_sites] is configured but + // [validator_list_keys] is not + Config c; + std::string toLoad(R"rippleConfig( +[validator_list_sites] +ripplevalidators.com +trustthesevalidators.gov +)rippleConfig"); + std::string error; + auto const expectedError = + "[validator_list_keys] config section is missing"; + try { + c.loadFromString (toLoad); + } catch (std::runtime_error& e) { + error = e.what(); + } + BEAST_EXPECT(error == expectedError); + } { // load from specified [validators_file] absolute path detail::ValidatorsTxtGuard const vtg ( @@ -550,6 +585,8 @@ nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 c.loadFromString (boost::str (cc % vtg.validatorsFile ())); BEAST_EXPECT(c.legacy ("validators_file") == vtg.validatorsFile ()); BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 8); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_SITES).values ().size () == 2); BEAST_EXPECT( c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 2); } @@ -566,6 +603,8 @@ nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 auto const& c (rcg.config ()); BEAST_EXPECT(c.legacy ("validators_file") == valFileName); BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 8); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_SITES).values ().size () == 2); BEAST_EXPECT( c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 2); } @@ -582,6 +621,8 @@ nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 auto const& c (rcg.config ()); BEAST_EXPECT(c.legacy ("validators_file") == valFilePath); BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 8); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_SITES).values ().size () == 2); BEAST_EXPECT( c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 2); } @@ -596,6 +637,8 @@ nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 auto const& c (rcg.config ()); BEAST_EXPECT(c.legacy ("validators_file").empty ()); BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 8); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_SITES).values ().size () == 2); BEAST_EXPECT( c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 2); } @@ -614,6 +657,8 @@ nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 auto const& c (rcg.config ()); BEAST_EXPECT(c.legacy ("validators_file") == vtg.validatorsFile ()); BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 8); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_SITES).values ().size () == 2); BEAST_EXPECT( c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 2); } @@ -635,6 +680,10 @@ n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA nHB1X37qrniVugfQcuBTAjswphC1drx7QjFFojJPZwKHHnt8kU7v nHUkAWDR4cB8AgPg7VXMX6et8xRTQb2KJfgv1aBEXozwrawRKgMB +[validator_list_sites] +ripplevalidators.com +trustthesevalidators.gov + [validator_list_keys] 021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566 )rippleConfig"); @@ -645,6 +694,8 @@ nHUkAWDR4cB8AgPg7VXMX6et8xRTQb2KJfgv1aBEXozwrawRKgMB c.loadFromString (boost::str (cc % vtg.validatorsFile ())); BEAST_EXPECT(c.legacy ("validators_file") == vtg.validatorsFile ()); BEAST_EXPECT(c.section (SECTION_VALIDATORS).values ().size () == 15); + BEAST_EXPECT( + c.section (SECTION_VALIDATOR_LIST_SITES).values ().size () == 4); BEAST_EXPECT( c.section (SECTION_VALIDATOR_LIST_KEYS).values ().size () == 3); } diff --git a/src/test/unity/app_test_unity.cpp b/src/test/unity/app_test_unity.cpp index e43b800ff6..bb6bde5ba0 100644 --- a/src/test/unity/app_test_unity.cpp +++ b/src/test/unity/app_test_unity.cpp @@ -45,5 +45,6 @@ #include #include #include +#include #include #include