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)
This commit is contained in:
wilsonianb
2016-11-02 16:14:31 -07:00
committed by seelabs
parent e823e60ca0
commit b45f45dcef
18 changed files with 1474 additions and 7 deletions

View File

@@ -1035,6 +1035,14 @@
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\misc\CanonicalTXSet.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\detail\Work.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\detail\WorkBase.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\detail\WorkPlain.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\detail\WorkSSL.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\FeeVote.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\misc\FeeVoteImpl.cpp">
@@ -1077,6 +1085,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\misc\impl\ValidatorSite.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\misc\LoadFeeTrack.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\Manifest.h">
@@ -1109,6 +1121,8 @@
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\ValidatorList.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\ValidatorSite.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\paths\AccountCurrencies.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -4275,6 +4289,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\test\app\ValidatorSite_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\test\basics\base_uint_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>

View File

@@ -121,6 +121,9 @@
<Filter Include="ripple\app\misc">
<UniqueIdentifier>{5A1509B2-871B-A7AC-1E60-544D3F398741}</UniqueIdentifier>
</Filter>
<Filter Include="ripple\app\misc\detail">
<UniqueIdentifier>{2919FCCC-A707-22B8-FFB4-89494A8AC070}</UniqueIdentifier>
</Filter>
<Filter Include="ripple\app\misc\impl">
<UniqueIdentifier>{C4BDB9F8-7DB7-E304-D286-098085D5D16E}</UniqueIdentifier>
</Filter>
@@ -1545,6 +1548,18 @@
<ClInclude Include="..\..\src\ripple\app\misc\CanonicalTXSet.h">
<Filter>ripple\app\misc</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\detail\Work.h">
<Filter>ripple\app\misc\detail</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\detail\WorkBase.h">
<Filter>ripple\app\misc\detail</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\detail\WorkPlain.h">
<Filter>ripple\app\misc\detail</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\detail\WorkSSL.h">
<Filter>ripple\app\misc\detail</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\FeeVote.h">
<Filter>ripple\app\misc</Filter>
</ClInclude>
@@ -1581,6 +1596,9 @@
<ClCompile Include="..\..\src\ripple\app\misc\impl\ValidatorList.cpp">
<Filter>ripple\app\misc\impl</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\misc\impl\ValidatorSite.cpp">
<Filter>ripple\app\misc\impl</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\misc\LoadFeeTrack.h">
<Filter>ripple\app\misc</Filter>
</ClInclude>
@@ -1620,6 +1638,9 @@
<ClInclude Include="..\..\src\ripple\app\misc\ValidatorList.h">
<Filter>ripple\app\misc</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\app\misc\ValidatorSite.h">
<Filter>ripple\app\misc</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\paths\AccountCurrencies.cpp">
<Filter>ripple\app\paths</Filter>
</ClCompile>
@@ -5040,6 +5061,9 @@
<ClCompile Include="..\..\src\test\app\ValidatorList_test.cpp">
<Filter>test\app</Filter>
</ClCompile>
<ClCompile Include="..\..\src\test\app\ValidatorSite_test.cpp">
<Filter>test\app</Filter>
</ClCompile>
<ClCompile Include="..\..\src\test\basics\base_uint_test.cpp">
<Filter>test\basics</Filter>
</ClCompile>

View File

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

View File

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

View File

@@ -45,6 +45,7 @@
#include <ripple/app/misc/TxQ.h>
#include <ripple/app/misc/Validations.h>
#include <ripple/app/misc/ValidatorList.h>
#include <ripple/app/misc/ValidatorSite.h>
#include <ripple/app/paths/Pathfinder.h>
#include <ripple/app/paths/PathRequests.h>
#include <ripple/app/tx/apply.h>
@@ -351,6 +352,7 @@ public:
std::unique_ptr <ManifestCache> validatorManifests_;
std::unique_ptr <ManifestCache> publisherManifests_;
std::unique_ptr <ValidatorList> validators_;
std::unique_ptr <ValidatorSite> validatorSites_;
std::unique_ptr <ServerHandler> serverHandler_;
std::unique_ptr <AmendmentTable> m_amendmentTable;
std::unique_ptr <LoadFeeTrack> mFeeTrack;
@@ -486,6 +488,9 @@ public:
*validatorManifests_, *publisherManifests_, *timeKeeper_,
logs_->journal("ValidatorList"), config_->VALIDATION_QUORUM))
, validatorSites_ (std::make_unique<ValidatorSite> (
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))
{

View File

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

View File

@@ -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 <ripple/app/misc/ValidatorList.h>
#include <ripple/app/misc/detail/Work.h>
#include <ripple/basics/Log.h>
#include <ripple/basics/StringUtilities.h>
#include <boost/asio.hpp>
#include <mutex>
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<detail::Work> work_;
boost::asio::basic_waitable_timer<clock_type> timer_;
// A list is currently being fetched from a site
std::atomic<bool> fetching_;
// One or more lists are due to be fetched
std::atomic<bool> pending_;
std::atomic<bool> stopping_;
// The configured list of URIs for fetching lists
std::vector<Site> 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<std::string> 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

View File

@@ -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 <beast/http/message.hpp>
#include <beast/http/string_body.hpp>
namespace ripple {
namespace detail {
using response_type =
beast::http::response<beast::http::string_body>;
class Work
{
public:
virtual ~Work() = default;
virtual void run() = 0;
virtual void cancel() = 0;
};
} // detail
} // ripple
#endif

View File

@@ -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 <ripple/app/misc/detail/Work.h>
#include <ripple/protocol/BuildInfo.h>
#include <beast/core/placeholders.hpp>
#include <beast/core/streambuf.hpp>
#include <beast/http/empty_body.hpp>
#include <beast/http/read.hpp>
#include <beast/http/write.hpp>
#include <boost/asio.hpp>
namespace ripple {
namespace detail {
template <class Impl>
class WorkBase
: public Work
{
protected:
using error_code = boost::system::error_code;
public:
using callback_type =
std::function<void(error_code const&, response_type&&)>;
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<beast::http::empty_body>;
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<Impl*>(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<class Impl>
WorkBase<Impl>::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<class Impl>
WorkBase<Impl>::~WorkBase()
{
if (cb_)
cb_ (make_error_code(boost::system::errc::not_a_socket),
std::move(res_));
}
template<class Impl>
void
WorkBase<Impl>::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<class Impl>
void
WorkBase<Impl>::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<class Impl>
void
WorkBase<Impl>::fail(error_code const& ec)
{
if (cb_)
{
cb_(ec, std::move(res_));
cb_ = nullptr;
}
}
template<class Impl>
void
WorkBase<Impl>::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<class Impl>
void
WorkBase<Impl>::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<class Impl>
void
WorkBase<Impl>::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<class Impl>
void
WorkBase<Impl>::onResponse(error_code const& ec)
{
if (ec)
return fail(ec);
assert(cb_);
cb_(ec, std::move(res_));
cb_ = nullptr;
}
} // detail
} // ripple
#endif

View File

@@ -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 <ripple/app/misc/detail/WorkBase.h>
namespace ripple {
namespace detail {
// Work over TCP/IP
class WorkPlain : public WorkBase<WorkPlain>
, public std::enable_shared_from_this<WorkPlain>
{
friend class WorkBase<WorkPlain>;
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

View File

@@ -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 <ripple/app/misc/detail/WorkBase.h>
#include <ripple/basics/contract.h>
#include <boost/asio/ssl.hpp>
#include <boost/bind.hpp>
#include <boost/format.hpp>
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<std::runtime_error> (
boost::str (boost::format (
"Failed to set_default_verify_paths: %s") %
ec.message ()));
}
}
};
// Work over SSL
class WorkSSL : public WorkBase<WorkSSL>
, public std::enable_shared_from_this<WorkSSL>
{
friend class WorkBase<WorkSSL>;
private:
using stream_type = boost::asio::ssl::stream<socket_type&>;
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

View File

@@ -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 <ripple/app/misc/detail/WorkPlain.h>
#include <ripple/app/misc/detail/WorkSSL.h>
#include <ripple/app/misc/ValidatorList.h>
#include <ripple/app/misc/ValidatorSite.h>
#include <ripple/basics/Slice.h>
#include <ripple/json/json_reader.h>
#include <beast/core/detail/base64.hpp>
#include <boost/regex.hpp>
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<std::mutex> 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<std::string> const& siteURIs)
{
JLOG (j_.debug()) <<
"Loading configured validator list sites";
std::lock_guard <std::mutex> 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 <std::mutex> lock{state_mutex_};
if (! timer_.expires_at().time_since_epoch().count())
setTimer ();
}
void
ValidatorSite::join ()
{
std::unique_lock<std::mutex> lock{state_mutex_};
cv_.wait(lock, [&]{ return ! pending_; });
}
void
ValidatorSite::stop()
{
std::unique_lock<std::mutex> 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 <std::mutex> 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 <std::mutex> lock{sites_mutex_};
sites_[siteIdx].nextRefresh =
clock_type::now() + DEFAULT_REFRESH_INTERVAL;
assert(! fetching_);
fetching_ = true;
std::shared_ptr<detail::Work> sp;
if (sites_[siteIdx].pUrl.scheme == "https")
{
sp = std::make_shared<detail::WorkSSL>(
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<detail::WorkPlain>(
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 <std::mutex> lock{sites_mutex_};
JLOG (j_.warn()) <<
"Request for validator list at " <<
sites_[siteIdx].uri << " returned " << res.status;
}
else if (! ec)
{
std::lock_guard <std::mutex> 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 <std::mutex> lock{state_mutex_};
fetching_ = false;
if (! stopping_)
setTimer ();
cv_.notify_all();
}
} // ripple

View File

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

View File

@@ -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::runtime_error> (
"[" + std::string(SECTION_VALIDATOR_LIST_KEYS) +
"] config section is missing");
}
}
{

View File

@@ -33,3 +33,4 @@
#include <ripple/app/misc/impl/Transaction.cpp>
#include <ripple/app/misc/impl/TxQ.cpp>
#include <ripple/app/misc/impl/ValidatorList.cpp>
#include <ripple/app/misc/impl/ValidatorSite.cpp>

View File

@@ -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 <beast/core/placeholders.hpp>
#include <beast/core/detail/base64.hpp>
#include <beast/http.hpp>
#include <ripple/app/misc/ValidatorSite.h>
#include <ripple/basics/Slice.h>
#include <ripple/basics/strHex.h>
#include <ripple/protocol/digest.h>
#include <ripple/protocol/HashPrefix.h>
#include <ripple/protocol/PublicKey.h>
#include <ripple/protocol/SecretKey.h>
#include <ripple/protocol/Sign.h>
#include <test/jtx.h>
#include <boost/utility/in_place_factory.hpp>
#include <boost/asio.hpp>
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<beast::http::string_body>;
using resp_type = beast::http::response<beast::http::string_body>;
using error_code = boost::system::error_code;
socket_type sock_;
boost::asio::ip::tcp::acceptor acceptor_;
std::string list_;
public:
http_sync_server(endpoint_type const& ep,
boost::asio::io_service& ios,
std::pair<PublicKey, SecretKey> keys,
std::string const& manifest,
int sequence,
std::size_t expiration,
int version,
std::vector <PublicKey> 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<char const*> (s.data()), s.size()));
}
void
testConfigLoad ()
{
testcase ("Config Load");
using namespace jtx;
Env env (*this);
auto trustedSites = std::make_unique<ValidatorSite> (
env.app().getIOService(), env.app().validators(), beast::Journal());
// load should accept empty sites list
std::vector<std::string> emptyCfgSites;
BEAST_EXPECT(trustedSites->load (emptyCfgSites));
// load should accept valid validator site uris
std::vector<std::string> 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<std::string> 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<std::string> 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<std::string> cfgPublishers({
strHex(publisherPublic1),
strHex(publisherPublic2)});
BEAST_EXPECT(trustedKeys.load (
emptyLocalKey, emptyCfgKeys, cfgPublishers));
auto constexpr listSize = 20;
std::vector<PublicKey> list1;
list1.reserve (listSize);
while (list1.size () < listSize)
list1.push_back (randomNode());
std::vector<PublicKey> 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<std::string> cfgSites(
{"http://127.0.0.1:" + std::to_string(port1) + "/validators"});
auto sites = std::make_unique<ValidatorSite> (
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<std::string> 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<ValidatorSite> (
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

View File

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

View File

@@ -45,5 +45,6 @@
#include <test/app/TrustAndBalance_test.cpp>
#include <test/app/TxQ_test.cpp>
#include <test/app/ValidatorList_test.cpp>
#include <test/app/ValidatorSite_test.cpp>
#include <test/app/SetTrust_test.cpp>
#include <test/app/Ticket_test.cpp>