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