Support UNLs with future effective dates:

* Creates a version 2 of the UNL file format allowing publishers to
  pre-publish the next UNL while the current one is still valid.
* Version 1 of the UNL file format is still valid and backward
  compatible.
* Also causes rippled to lock down if it has no valid UNLs, similar to
  being amendment blocked, except reversible.
* Resolves #3548
* Resolves #3470
This commit is contained in:
Edward Hennis
2020-09-09 18:51:08 -04:00
parent 54da532ace
commit 4b9d3ca7de
31 changed files with 3980 additions and 932 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -53,6 +53,7 @@ realValidatorContents()
}
auto constexpr default_expires = std::chrono::seconds{3600};
auto constexpr default_effective_overlap = std::chrono::seconds{30};
} // namespace detail
class ValidatorSite_test : public beast::unit_test::suite
@@ -135,6 +136,8 @@ private:
bool failApply = false;
int serverVersion = 1;
std::chrono::seconds expiresFromNow = detail::default_expires;
std::chrono::seconds effectiveOverlap =
detail::default_effective_overlap;
int expectedRefreshMin = 0;
};
void
@@ -146,13 +149,17 @@ private:
boost::adaptors::transformed(
[](FetchListConfig const& cfg) {
return cfg.path +
(cfg.ssl ? " [https]" : " [http]");
(cfg.ssl ? " [https] v" : " [http] v") +
std::to_string(cfg.serverVersion) +
" " + cfg.msg;
}),
", ");
using namespace jtx;
using namespace std::chrono_literals;
Env env(*this);
auto& trustedKeys = env.app().validators();
env.timeKeeper().set(env.timeKeeper().now() + 30s);
test::StreamSink sink;
beast::Journal journal{sink};
@@ -184,10 +191,17 @@ private:
while (item.list.size() < listSize)
item.list.push_back(TrustedPublisherServer::randomValidator());
NetClock::time_point const expires =
env.timeKeeper().now() + cfg.expiresFromNow;
NetClock::time_point const effective2 =
expires - cfg.effectiveOverlap;
NetClock::time_point const expires2 =
effective2 + cfg.expiresFromNow;
item.server = make_TrustedPublisherServer(
env.app().getIOService(),
item.list,
env.timeKeeper().now() + cfg.expiresFromNow,
expires,
{{effective2, expires2}},
cfg.ssl,
cfg.serverVersion);
cfgPublishers.push_back(strHex(item.server->publisherPublic()));
@@ -201,7 +215,6 @@ private:
BEAST_EXPECT(
trustedKeys.load(emptyLocalKey, emptyCfgKeys, cfgPublishers));
using namespace std::chrono_literals;
// Normally, tests will only need a fraction of this time,
// but sometimes DNS resolution takes an inordinate amount
// of time, so the test will just wait.
@@ -381,8 +394,15 @@ public:
{
// fetch single site
testFetchList({{"/validators", "", ssl}});
testFetchList({{"/validators2", "", ssl}});
// fetch multiple sites
testFetchList({{"/validators", "", ssl}, {"/validators", "", ssl}});
testFetchList(
{{"/validators", "", ssl}, {"/validators2", "", ssl}});
testFetchList(
{{"/validators2", "", ssl}, {"/validators", "", ssl}});
testFetchList(
{{"/validators2", "", ssl}, {"/validators2", "", ssl}});
// fetch single site with single redirects
testFetchList({{"/redirect_once/301", "", ssl}});
testFetchList({{"/redirect_once/302", "", ssl}});
@@ -391,6 +411,19 @@ public:
// one redirect, one not
testFetchList(
{{"/validators", "", ssl}, {"/redirect_once/302", "", ssl}});
testFetchList(
{{"/validators2", "", ssl}, {"/redirect_once/302", "", ssl}});
// UNLs with a "gap" between validUntil of one and validFrom of the
// next
testFetchList(
{{"/validators2",
"",
ssl,
false,
false,
1,
detail::default_expires,
std::chrono::seconds{-90}}});
// fetch single site with undending redirect (fails to load)
testFetchList(
{{"/redirect_forever/301",
@@ -418,6 +451,14 @@ public:
ssl,
true,
true}});
// one undending redirect, one not
testFetchList(
{{"/validators2", "", ssl},
{"/redirect_forever/302",
"Exceeded max redirects",
ssl,
true,
true}});
// invalid redir Location
testFetchList(
{{"/redirect_to/ftp://invalid-url/302",
@@ -438,6 +479,12 @@ public:
ssl,
true,
true}});
testFetchList(
{{"/validators2/bad",
"Unable to parse JSON response",
ssl,
true,
true}});
// error status returned
testFetchList(
{{"/bad-resource", "returned bad status", ssl, true, true}});
@@ -455,30 +502,96 @@ public:
ssl,
true,
true}});
testFetchList(
{{"/validators2/missing",
"Missing fields in JSON response",
ssl,
true,
true}});
// timeout
testFetchList({{"/sleep/13", "took too long", ssl, true, true}});
// bad manifest format using known versions
// * Retrieves a v1 formatted list claiming version 2
testFetchList(
{{"/validators", "Missing fields", ssl, true, true, 2}});
// * Retrieves a v2 formatted list claiming version 1
testFetchList(
{{"/validators2", "Missing fields", ssl, true, true, 0}});
// bad manifest version
// Because versions other than 1 are treated as v2, the v1
// list won't have the blobs_v2 fields, and thus will claim to have
// missing fields
testFetchList(
{{"/validators", "Unsupported version", ssl, false, true, 4}});
using namespace std::chrono_literals;
// get old validator list
{{"/validators", "Missing fields", ssl, true, true, 4}});
testFetchList(
{{"/validators",
"Stale validator list",
{{"/validators2",
"1 unsupported version",
ssl,
false,
true,
1,
0s}});
// force an out-of-range expiration value
4}});
using namespace std::chrono_literals;
// get expired validator list
testFetchList(
{{"/validators",
"Invalid validator list",
"Applied 1 expired validator list(s)",
ssl,
false,
false,
1,
0s}});
testFetchList(
{{"/validators2",
"Applied 1 expired validator list(s)",
ssl,
false,
false,
1,
0s,
-1s}});
// force an out-of-range validUntil value
testFetchList(
{{"/validators",
"1 invalid validator list(s)",
ssl,
false,
true,
1,
std::chrono::seconds{Json::Value::maxInt + 1}}});
// force an out-of-range validUntil value on the future list
// The first list is accepted. The second fails. The parser
// returns the "best" result, so this looks like a success.
testFetchList(
{{"/validators2",
"",
ssl,
false,
false,
1,
std::chrono::seconds{Json::Value::maxInt - 300},
299s}});
// force an out-of-range validFrom value
// The first list is accepted. The second fails. The parser
// returns the "best" result, so this looks like a success.
testFetchList(
{{"/validators2",
"",
ssl,
false,
false,
1,
std::chrono::seconds{Json::Value::maxInt - 300},
301s}});
// force an out-of-range validUntil value on _both_ lists
testFetchList(
{{"/validators2",
"2 invalid validator list(s)",
ssl,
false,
true,
1,
std::chrono::seconds{Json::Value::maxInt + 1},
std::chrono::seconds{Json::Value::maxInt - 6000}}});
// verify refresh intervals are properly clamped
testFetchList(
{{"/validators/refresh/0",
@@ -488,6 +601,17 @@ public:
false,
1,
detail::default_expires,
detail::default_effective_overlap,
1}}); // minimum of 1 minute
testFetchList(
{{"/validators2/refresh/0",
"",
ssl,
false,
false,
1,
detail::default_expires,
detail::default_effective_overlap,
1}}); // minimum of 1 minute
testFetchList(
{{"/validators/refresh/10",
@@ -497,6 +621,17 @@ public:
false,
1,
detail::default_expires,
detail::default_effective_overlap,
10}}); // 10 minutes is fine
testFetchList(
{{"/validators2/refresh/10",
"",
ssl,
false,
false,
1,
detail::default_expires,
detail::default_effective_overlap,
10}}); // 10 minutes is fine
testFetchList(
{{"/validators/refresh/2000",
@@ -506,6 +641,17 @@ public:
false,
1,
detail::default_expires,
detail::default_effective_overlap,
60 * 24}}); // max of 24 hours
testFetchList(
{{"/validators2/refresh/2000",
"",
ssl,
false,
false,
1,
detail::default_expires,
detail::default_effective_overlap,
60 * 24}}); // max of 24 hours
}
testFileURLs();

View File

@@ -1860,7 +1860,12 @@ class NegativeUNLVoteFilterValidations_test : public beast::unit_test::suite
auto& local = *nUnlKeys.begin();
std::vector<std::string> cfgPublishers;
validators.load(local, cfgKeys, cfgPublishers);
validators.updateTrusted(activeValidators);
validators.updateTrusted(
activeValidators,
env.timeKeeper().now(),
env.app().getOPs(),
env.app().overlay(),
env.app().getHashRouter());
BEAST_EXPECT(validators.getTrustedMasterKeys().size() == numNodes);
validators.setNegativeUNL(nUnlKeys);
BEAST_EXPECT(validators.getNegativeUNL().size() == negUnlSize);

View File

@@ -57,7 +57,12 @@ class TrustedPublisherServer
socket_type sock_;
endpoint_type ep_;
boost::asio::ip::tcp::acceptor acceptor_;
// Generates a version 1 validator list, using the int parameter as the
// actual version.
std::function<std::string(int)> getList_;
// Generates a version 2 validator list, using the int parameter as the
// actual version.
std::function<std::string(int)> getList2_;
// The SSL context is required, and holds certificates
bool useSSL_;
@@ -91,6 +96,18 @@ class TrustedPublisherServer
sslCtx_.use_tmp_dh(boost::asio::buffer(dh().data(), dh().size()));
}
struct BlobInfo
{
BlobInfo(std::string b, std::string s) : blob(b), signature(s)
{
}
// base-64 encoded JSON containing the validator list.
std::string blob;
// hex-encoded signature of the blob using the publisher's signing key
std::string signature;
};
public:
struct Validator
{
@@ -144,14 +161,19 @@ public:
1)};
}
// TrustedPublisherServer must be accessed through a shared_ptr
// TrustedPublisherServer must be accessed through a shared_ptr.
// This constructor is only public so std::make_shared has access.
// The function`make_TrustedPublisherServer` should be used to create
// instances.
// The `futures` member is expected to be structured as
// effective / expiration time point pairs for use in version 2 UNLs
TrustedPublisherServer(
boost::asio::io_context& ioc,
std::vector<Validator> const& validators,
NetClock::time_point expiration,
NetClock::time_point validUntil,
std::vector<
std::pair<NetClock::time_point, NetClock::time_point>> const&
futures,
bool useSSL = false,
int version = 1,
bool immediateStart = true,
@@ -170,29 +192,80 @@ public:
auto const manifest = makeManifestString(
publisherPublic_, publisherSecret_, keys.first, keys.second, 1);
std::string data = "{\"sequence\":" + std::to_string(sequence) +
",\"expiration\":" +
std::to_string(expiration.time_since_epoch().count()) +
",\"validators\":[";
std::vector<BlobInfo> blobInfo;
blobInfo.reserve(futures.size() + 1);
auto const [data, blob] = [&]() -> std::pair<std::string, std::string> {
// Builds the validator list, then encodes it into a blob.
std::string data = "{\"sequence\":" + std::to_string(sequence) +
",\"expiration\":" +
std::to_string(validUntil.time_since_epoch().count()) +
",\"validators\":[";
for (auto const& val : validators)
{
data += "{\"validation_public_key\":\"" + strHex(val.masterPublic) +
"\",\"manifest\":\"" + val.manifest + "\"},";
}
data.pop_back();
data += "]}";
std::string blob = base64_encode(data);
auto const sig = sign(keys.first, keys.second, makeSlice(data));
getList_ = [blob, sig, manifest, version](int interval) {
for (auto const& val : validators)
{
data += "{\"validation_public_key\":\"" +
strHex(val.masterPublic) + "\",\"manifest\":\"" +
val.manifest + "\"},";
}
data.pop_back();
data += "]}";
std::string blob = base64_encode(data);
return std::make_pair(data, blob);
}();
auto const sig = strHex(sign(keys.first, keys.second, makeSlice(data)));
blobInfo.emplace_back(blob, sig);
getList_ = [blob = blob, sig, manifest, version](int interval) {
// Build the contents of a version 1 format UNL file
std::stringstream l;
l << "{\"blob\":\"" << blob << "\""
<< ",\"signature\":\"" << strHex(sig) << "\""
<< ",\"signature\":\"" << sig << "\""
<< ",\"manifest\":\"" << manifest << "\""
<< ",\"refresh_interval\": " << interval
<< ",\"version\":" << version << '}';
return l.str();
};
for (auto const& future : futures)
{
std::string data = "{\"sequence\":" + std::to_string(++sequence) +
",\"effective\":" +
std::to_string(future.first.time_since_epoch().count()) +
",\"expiration\":" +
std::to_string(future.second.time_since_epoch().count()) +
",\"validators\":[";
// Use the same set of validators for simplicity
for (auto const& val : validators)
{
data += "{\"validation_public_key\":\"" +
strHex(val.masterPublic) + "\",\"manifest\":\"" +
val.manifest + "\"},";
}
data.pop_back();
data += "]}";
std::string blob = base64_encode(data);
auto const sig =
strHex(sign(keys.first, keys.second, makeSlice(data)));
blobInfo.emplace_back(blob, sig);
}
getList2_ = [blobInfo, manifest, version](int interval) {
// Build the contents of a version 2 format UNL file
// Use `version + 1` to get 2 for most tests, but have
// a "bad" version number for tests that provide an override.
std::stringstream l;
for (auto const& info : blobInfo)
{
l << "{\"blob\":\"" << info.blob << "\""
<< ",\"signature\":\"" << info.signature << "\"},";
}
std::string blobs = l.str();
blobs.pop_back();
l.str(std::string());
l << "{\"blobs_v2\": [ " << blobs << "],\"manifest\":\"" << manifest
<< "\""
<< ",\"refresh_interval\": " << interval
<< ",\"version\":" << (version + 1) << '}';
return l.str();
};
if (useSSL_)
{
@@ -505,7 +578,26 @@ private:
res.keep_alive(req.keep_alive());
bool prepare = true;
if (boost::starts_with(path, "/validators"))
if (boost::starts_with(path, "/validators2"))
{
res.result(http::status::ok);
res.insert("Content-Type", "application/json");
if (path == "/validators2/bad")
res.body() = "{ 'bad': \"2']";
else if (path == "/validators2/missing")
res.body() = "{\"version\": 2}";
else
{
int refresh = 5;
constexpr char const* refreshPrefix =
"/validators2/refresh/";
if (boost::starts_with(path, refreshPrefix))
refresh = boost::lexical_cast<unsigned int>(
path.substr(strlen(refreshPrefix)));
res.body() = getList2_(refresh);
}
}
else if (boost::starts_with(path, "/validators"))
{
res.result(http::status::ok);
res.insert("Content-Type", "application/json");
@@ -516,9 +608,11 @@ private:
else
{
int refresh = 5;
if (boost::starts_with(path, "/validators/refresh"))
constexpr char const* refreshPrefix =
"/validators/refresh/";
if (boost::starts_with(path, refreshPrefix))
refresh = boost::lexical_cast<unsigned int>(
path.substr(20));
path.substr(strlen(refreshPrefix)));
res.body() = getList_(refresh);
}
}
@@ -618,14 +712,16 @@ inline std::shared_ptr<TrustedPublisherServer>
make_TrustedPublisherServer(
boost::asio::io_context& ioc,
std::vector<TrustedPublisherServer::Validator> const& validators,
NetClock::time_point expiration,
NetClock::time_point validUntil,
std::vector<std::pair<NetClock::time_point, NetClock::time_point>> const&
futures,
bool useSSL = false,
int version = 1,
bool immediateStart = true,
int sequence = 1)
{
auto const r = std::make_shared<TrustedPublisherServer>(
ioc, validators, expiration, useSSL, version, sequence);
ioc, validators, validUntil, futures, useSSL, version, sequence);
if (immediateStart)
r->start();
return r;

View File

@@ -40,6 +40,8 @@ class DatabaseDownloader_test : public beast::unit_test::suite
env.app().getIOService(),
list,
env.timeKeeper().now() + std::chrono::seconds{3600},
// No future VLs
{},
ssl);
}

View File

@@ -344,11 +344,43 @@ public:
return list;
}
std::shared_ptr<protocol::TMValidatorListCollection>
buildValidatorListCollection()
{
auto list = std::make_shared<protocol::TMValidatorListCollection>();
auto master = randomKeyPair(KeyType::ed25519);
auto signing = randomKeyPair(KeyType::ed25519);
STObject st(sfGeneric);
st[sfSequence] = 0;
st[sfPublicKey] = std::get<0>(master);
st[sfSigningPubKey] = std::get<0>(signing);
st[sfDomain] = makeSlice(std::string("example.com"));
sign(
st,
HashPrefix::manifest,
KeyType::ed25519,
std::get<1>(master),
sfMasterSignature);
sign(st, HashPrefix::manifest, KeyType::ed25519, std::get<1>(signing));
Serializer s;
st.add(s);
list->set_manifest(s.data(), s.size());
list->set_version(4);
STObject signature(sfSignature);
ripple::sign(
st, HashPrefix::manifest, KeyType::ed25519, std::get<1>(signing));
Serializer s1;
st.add(s1);
auto& blob = *list->add_blobs();
blob.set_signature(s1.data(), s1.size());
blob.set_blob(strHex(s.getString()));
return list;
}
void
testProtocol()
{
testcase("Message Compression");
auto thresh = beast::severities::Severity::kInfo;
auto logs = std::make_unique<Logs>(thresh);
@@ -359,6 +391,7 @@ public:
protocol::TMLedgerData ledger_data;
protocol::TMGetObjectByHash get_object;
protocol::TMValidatorList validator_list;
protocol::TMValidatorListCollection validator_list_collection;
// 4.5KB
doTest(buildManifests(20), protocol::mtMANIFESTS, 4, "TMManifests20");
@@ -418,6 +451,11 @@ public:
protocol::mtVALIDATORLIST,
4,
"TMValidatorList");
doTest(
buildValidatorListCollection(),
protocol::mtVALIDATORLISTCOLLECTION,
4,
"TMValidatorListCollection");
}
void

View File

@@ -46,6 +46,8 @@ class ShardArchiveHandler_test : public beast::unit_test::suite
env.app().getIOService(),
list,
env.timeKeeper().now() + std::chrono::seconds{3600},
// No future VLs
{},
ssl);
}

View File

@@ -189,12 +189,20 @@ public:
// Manage single-thread io_service for server.
BasicApp worker{1};
using namespace std::chrono_literals;
NetClock::time_point const expiration{3600s};
NetClock::time_point const validUntil{3600s};
NetClock::time_point const validFrom2{validUntil - 60s};
NetClock::time_point const validUntil2{validFrom2 + 3600s};
auto server = make_TrustedPublisherServer(
worker.get_io_service(), validators, expiration, false, 1, false);
worker.get_io_service(),
validators,
validUntil,
{{validFrom2, validUntil2}},
false,
1,
false);
//----------------------------------------------------------------------
// Publisher list site unavailable
// Publisher list site unavailable v1
{
// Publisher site information
using namespace std::string_literals;
@@ -261,11 +269,78 @@ public:
}
}
}
// Publisher list site unavailable v2
{
// Publisher site information
using namespace std::string_literals;
std::string siteURI =
"http://"s + getEnvLocalhostAddr() + ":1234/validators2";
Env env{
*this,
envconfig([&](std::unique_ptr<Config> cfg) {
cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI);
cfg->section(SECTION_VALIDATOR_LIST_KEYS)
.append(strHex(server->publisherPublic()));
return cfg;
}),
};
env.app().validatorSites().start();
env.app().validatorSites().join();
{
auto const jrr = env.rpc("server_info")[jss::result];
BEAST_EXPECT(
jrr[jss::info][jss::validator_list][jss::expiration] ==
"unknown");
}
{
auto const jrr = env.rpc("server_state")[jss::result];
BEAST_EXPECT(
jrr[jss::state][jss::validator_list_expires].asInt() == 0);
}
{
auto const jrr = env.rpc("validators")[jss::result];
BEAST_EXPECT(
jrr[jss::validation_quorum].asUInt() ==
std::numeric_limits<std::uint32_t>::max());
BEAST_EXPECT(jrr[jss::local_static_keys].size() == 0);
BEAST_EXPECT(jrr[jss::trusted_validator_keys].size() == 0);
BEAST_EXPECT(
jrr[jss::validator_list][jss::expiration] == "unknown");
if (BEAST_EXPECT(jrr[jss::publisher_lists].size() == 1))
{
auto jp = jrr[jss::publisher_lists][0u];
BEAST_EXPECT(jp[jss::available] == false);
BEAST_EXPECT(jp[jss::list].size() == 0);
BEAST_EXPECT(!jp.isMember(jss::seq));
BEAST_EXPECT(!jp.isMember(jss::expiration));
BEAST_EXPECT(!jp.isMember(jss::version));
BEAST_EXPECT(
jp[jss::pubkey_publisher] ==
strHex(server->publisherPublic()));
}
BEAST_EXPECT(jrr[jss::signing_keys].size() == 0);
}
{
auto const jrr = env.rpc("validator_list_sites")[jss::result];
if (BEAST_EXPECT(jrr[jss::validator_sites].size() == 1))
{
auto js = jrr[jss::validator_sites][0u];
BEAST_EXPECT(js[jss::refresh_interval_min].asUInt() == 5);
BEAST_EXPECT(js[jss::uri] == siteURI);
BEAST_EXPECT(js.isMember(jss::last_refresh_time));
BEAST_EXPECT(js[jss::last_refresh_status] == "invalid");
}
}
}
//----------------------------------------------------------------------
// Publisher list site available
server->start();
// Publisher list site available v1
{
server->start();
std::stringstream uri;
uri << "http://" << server->local_endpoint() << "/validators";
auto siteURI = uri.str();
@@ -286,26 +361,31 @@ public:
for (auto const& val : validators)
startKeys.insert(calcNodeID(val.masterPublic));
env.app().validators().updateTrusted(startKeys);
env.app().validators().updateTrusted(
startKeys,
env.timeKeeper().now(),
env.app().getOPs(),
env.app().overlay(),
env.app().getHashRouter());
{
auto const jrr = env.rpc("server_info")[jss::result];
BEAST_EXPECT(
jrr[jss::info][jss::validator_list][jss::expiration] ==
to_string(expiration));
to_string(validUntil));
}
{
auto const jrr = env.rpc("server_state")[jss::result];
BEAST_EXPECT(
jrr[jss::state][jss::validator_list_expires].asUInt() ==
expiration.time_since_epoch().count());
validUntil.time_since_epoch().count());
}
{
auto const jrr = env.rpc("validators")[jss::result];
BEAST_EXPECT(jrr[jss::validation_quorum].asUInt() == 2);
BEAST_EXPECT(
jrr[jss::validator_list][jss::expiration] ==
to_string(expiration));
to_string(validUntil));
BEAST_EXPECT(jrr[jss::local_static_keys].size() == 0);
BEAST_EXPECT(
@@ -334,7 +414,7 @@ public:
BEAST_EXPECT(
jp[jss::pubkey_publisher] ==
strHex(server->publisherPublic()));
BEAST_EXPECT(jp[jss::expiration] == to_string(expiration));
BEAST_EXPECT(jp[jss::expiration] == to_string(validUntil));
BEAST_EXPECT(jp[jss::version] == 1);
}
auto jsk = jrr[jss::signing_keys];
@@ -361,6 +441,129 @@ public:
}
}
}
// Publisher list site available v2
{
std::stringstream uri;
uri << "http://" << server->local_endpoint() << "/validators2";
auto siteURI = uri.str();
Env env{
*this,
envconfig([&](std::unique_ptr<Config> cfg) {
cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI);
cfg->section(SECTION_VALIDATOR_LIST_KEYS)
.append(strHex(server->publisherPublic()));
return cfg;
}),
};
env.app().validatorSites().start();
env.app().validatorSites().join();
hash_set<NodeID> startKeys;
for (auto const& val : validators)
startKeys.insert(calcNodeID(val.masterPublic));
env.app().validators().updateTrusted(
startKeys,
env.timeKeeper().now(),
env.app().getOPs(),
env.app().overlay(),
env.app().getHashRouter());
{
auto const jrr = env.rpc("server_info")[jss::result];
BEAST_EXPECT(
jrr[jss::info][jss::validator_list][jss::expiration] ==
to_string(validUntil2));
}
{
auto const jrr = env.rpc("server_state")[jss::result];
BEAST_EXPECT(
jrr[jss::state][jss::validator_list_expires].asUInt() ==
validUntil2.time_since_epoch().count());
}
{
auto const jrr = env.rpc("validators")[jss::result];
BEAST_EXPECT(jrr[jss::validation_quorum].asUInt() == 2);
BEAST_EXPECT(
jrr[jss::validator_list][jss::expiration] ==
to_string(validUntil2));
BEAST_EXPECT(jrr[jss::local_static_keys].size() == 0);
BEAST_EXPECT(
jrr[jss::trusted_validator_keys].size() ==
expectedKeys.size());
for (auto const& jKey : jrr[jss::trusted_validator_keys])
{
BEAST_EXPECT(expectedKeys.count(jKey.asString()) == 1);
}
if (BEAST_EXPECT(jrr[jss::publisher_lists].size() == 1))
{
auto jp = jrr[jss::publisher_lists][0u];
BEAST_EXPECT(jp[jss::available] == true);
if (BEAST_EXPECT(jp[jss::list].size() == 2))
{
// check entries
std::set<std::string> foundKeys;
for (auto const& k : jp[jss::list])
{
foundKeys.insert(k.asString());
}
BEAST_EXPECT(foundKeys == expectedKeys);
}
BEAST_EXPECT(jp[jss::seq].asUInt() == 1);
BEAST_EXPECT(
jp[jss::pubkey_publisher] ==
strHex(server->publisherPublic()));
BEAST_EXPECT(jp[jss::expiration] == to_string(validUntil));
BEAST_EXPECT(jp[jss::version] == 2);
if (BEAST_EXPECT(jp.isMember(jss::remaining)) &&
BEAST_EXPECT(jp[jss::remaining].isArray()) &&
BEAST_EXPECT(jp[jss::remaining].size() == 1))
{
auto const& r = jp[jss::remaining][0u];
if (BEAST_EXPECT(r[jss::list].size() == 2))
{
// check entries
std::set<std::string> foundKeys;
for (auto const& k : r[jss::list])
{
foundKeys.insert(k.asString());
}
BEAST_EXPECT(foundKeys == expectedKeys);
}
BEAST_EXPECT(r[jss::seq].asUInt() == 2);
BEAST_EXPECT(
r[jss::effective] == to_string(validFrom2));
BEAST_EXPECT(
r[jss::expiration] == to_string(validUntil2));
}
}
auto jsk = jrr[jss::signing_keys];
BEAST_EXPECT(jsk.size() == 2);
for (auto const& val : validators)
{
BEAST_EXPECT(jsk.isMember(toStr(val.masterPublic)));
BEAST_EXPECT(
jsk[toStr(val.masterPublic)] ==
toStr(val.signingPublic));
}
}
{
auto const jrr = env.rpc("validator_list_sites")[jss::result];
if (BEAST_EXPECT(jrr[jss::validator_sites].size() == 1))
{
auto js = jrr[jss::validator_sites][0u];
BEAST_EXPECT(js[jss::refresh_interval_min].asUInt() == 5);
BEAST_EXPECT(js[jss::uri] == siteURI);
BEAST_EXPECT(js[jss::last_refresh_status] == "accepted");
// The actual time of the update will vary run to run, so
// just verify the time is there
BEAST_EXPECT(js.isMember(jss::last_refresh_time));
}
}
}
}
void