mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
A few unit tests have historically generated a lot of noise to the console from log writes. This noise was not useful and made it harder to locate actual test failures. By changing the log level of these tests from - severities::kError to - severities::kDisabled it was possible to remove that noise coming from the logs.
2432 lines
92 KiB
C++
2432 lines
92 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright 2015 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/ValidatorList.h>
|
|
#include <ripple/basics/Slice.h>
|
|
#include <ripple/basics/base64.h>
|
|
#include <ripple/basics/strHex.h>
|
|
#include <ripple/overlay/impl/ProtocolMessage.h>
|
|
#include <ripple/protocol/HashPrefix.h>
|
|
#include <ripple/protocol/PublicKey.h>
|
|
#include <ripple/protocol/SecretKey.h>
|
|
#include <ripple/protocol/Sign.h>
|
|
#include <ripple/protocol/digest.h>
|
|
#include <ripple/protocol/jss.h>
|
|
#include <ripple/protocol/messages.h>
|
|
#include <test/jtx.h>
|
|
|
|
#include <boost/beast/core/multi_buffer.hpp>
|
|
|
|
namespace ripple {
|
|
namespace test {
|
|
|
|
class ValidatorList_test : public beast::unit_test::suite
|
|
{
|
|
private:
|
|
struct Validator
|
|
{
|
|
PublicKey masterPublic;
|
|
PublicKey signingPublic;
|
|
std::string manifest;
|
|
};
|
|
|
|
static PublicKey
|
|
randomNode()
|
|
{
|
|
return derivePublicKey(KeyType::secp256k1, randomSecretKey());
|
|
}
|
|
|
|
static PublicKey
|
|
randomMasterKey()
|
|
{
|
|
return derivePublicKey(KeyType::ed25519, randomSecretKey());
|
|
}
|
|
|
|
static std::string
|
|
makeManifestString(
|
|
PublicKey const& pk,
|
|
SecretKey const& sk,
|
|
PublicKey const& spk,
|
|
SecretKey const& ssk,
|
|
int seq)
|
|
{
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = seq;
|
|
st[sfPublicKey] = pk;
|
|
|
|
if (seq != std::numeric_limits<std::uint32_t>::max())
|
|
{
|
|
st[sfSigningPubKey] = spk;
|
|
sign(st, HashPrefix::manifest, *publicKeyType(spk), ssk);
|
|
}
|
|
|
|
sign(
|
|
st,
|
|
HashPrefix::manifest,
|
|
*publicKeyType(pk),
|
|
sk,
|
|
sfMasterSignature);
|
|
|
|
Serializer s;
|
|
st.add(s);
|
|
|
|
return std::string(static_cast<char const*>(s.data()), s.size());
|
|
}
|
|
|
|
static std::string
|
|
makeRevocationString(PublicKey const& pk, SecretKey const& sk)
|
|
{
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = std::numeric_limits<std::uint32_t>::max();
|
|
st[sfPublicKey] = pk;
|
|
|
|
sign(
|
|
st,
|
|
HashPrefix::manifest,
|
|
*publicKeyType(pk),
|
|
sk,
|
|
sfMasterSignature);
|
|
|
|
Serializer s;
|
|
st.add(s);
|
|
|
|
return std::string(static_cast<char const*>(s.data()), s.size());
|
|
}
|
|
|
|
static Validator
|
|
randomValidator()
|
|
{
|
|
auto const secret = randomSecretKey();
|
|
auto const masterPublic = derivePublicKey(KeyType::ed25519, secret);
|
|
auto const signingKeys = randomKeyPair(KeyType::secp256k1);
|
|
return {
|
|
masterPublic,
|
|
signingKeys.first,
|
|
base64_encode(makeManifestString(
|
|
masterPublic,
|
|
secret,
|
|
signingKeys.first,
|
|
signingKeys.second,
|
|
1))};
|
|
}
|
|
|
|
std::string
|
|
makeList(
|
|
std::vector<Validator> const& validators,
|
|
std::size_t sequence,
|
|
std::size_t validUntil,
|
|
std::optional<std::size_t> validFrom = {})
|
|
{
|
|
std::string data = "{\"sequence\":" + std::to_string(sequence) +
|
|
",\"expiration\":" + std::to_string(validUntil);
|
|
if (validFrom)
|
|
data += ",\"effective\":" + std::to_string(*validFrom);
|
|
data += ",\"validators\":[";
|
|
|
|
for (auto const& val : validators)
|
|
{
|
|
data += "{\"validation_public_key\":\"" + strHex(val.masterPublic) +
|
|
"\",\"manifest\":\"" + val.manifest + "\"},";
|
|
}
|
|
|
|
data.pop_back();
|
|
data += "]}";
|
|
return base64_encode(data);
|
|
}
|
|
|
|
std::string
|
|
signList(
|
|
std::string const& blob,
|
|
std::pair<PublicKey, SecretKey> const& keys)
|
|
{
|
|
auto const data = base64_decode(blob);
|
|
return strHex(sign(keys.first, keys.second, makeSlice(data)));
|
|
}
|
|
|
|
static hash_set<NodeID>
|
|
asNodeIDs(std::initializer_list<PublicKey> const& pks)
|
|
{
|
|
hash_set<NodeID> res;
|
|
res.reserve(pks.size());
|
|
for (auto const& pk : pks)
|
|
res.insert(calcNodeID(pk));
|
|
return res;
|
|
}
|
|
|
|
void
|
|
checkResult(
|
|
ValidatorList::PublisherListStats const& result,
|
|
PublicKey pubKey,
|
|
ListDisposition expectedWorst,
|
|
ListDisposition expectedBest)
|
|
{
|
|
BEAST_EXPECT(
|
|
result.bestDisposition() > ListDisposition::same_sequence ||
|
|
(result.publisherKey && *result.publisherKey == pubKey));
|
|
BEAST_EXPECT(result.bestDisposition() == expectedBest);
|
|
BEAST_EXPECT(result.worstDisposition() == expectedWorst);
|
|
}
|
|
|
|
void
|
|
testGenesisQuorum()
|
|
{
|
|
testcase("Genesis Quorum");
|
|
|
|
ManifestCache manifests;
|
|
jtx::Env env(*this);
|
|
auto& app = env.app();
|
|
{
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
BEAST_EXPECT(trustedKeys->quorum() == 1);
|
|
}
|
|
{
|
|
std::size_t minQuorum = 0;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal,
|
|
minQuorum);
|
|
BEAST_EXPECT(trustedKeys->quorum() == minQuorum);
|
|
}
|
|
}
|
|
|
|
void
|
|
testConfigLoad()
|
|
{
|
|
testcase("Config Load");
|
|
|
|
jtx::Env env(
|
|
*this, jtx::envconfig(), nullptr, beast::severities::kDisabled);
|
|
auto& app = env.app();
|
|
PublicKey emptyLocalKey;
|
|
std::vector<std::string> const emptyCfgKeys;
|
|
std::vector<std::string> const emptyCfgPublishers;
|
|
|
|
auto const localSigningKeys = randomKeyPair(KeyType::secp256k1);
|
|
auto const localSigningPublicOuter = localSigningKeys.first;
|
|
auto const localSigningSecret = localSigningKeys.second;
|
|
auto const localMasterSecret = randomSecretKey();
|
|
auto const localMasterPublic =
|
|
derivePublicKey(KeyType::ed25519, localMasterSecret);
|
|
|
|
std::string const cfgManifest(makeManifestString(
|
|
localMasterPublic,
|
|
localMasterSecret,
|
|
localSigningPublicOuter,
|
|
localSigningSecret,
|
|
1));
|
|
|
|
auto format = [](PublicKey const& publicKey,
|
|
char const* comment = nullptr) {
|
|
auto ret = toBase58(TokenType::NodePublic, publicKey);
|
|
|
|
if (comment)
|
|
ret += comment;
|
|
|
|
return ret;
|
|
};
|
|
|
|
std::vector<PublicKey> configList;
|
|
configList.reserve(8);
|
|
|
|
while (configList.size() != 8)
|
|
configList.push_back(randomNode());
|
|
|
|
// Correct configuration
|
|
std::vector<std::string> cfgKeys(
|
|
{format(configList[0]),
|
|
format(configList[1], " Comment"),
|
|
format(configList[2], " Multi Word Comment"),
|
|
format(configList[3], " Leading Whitespace"),
|
|
format(configList[4], " Trailing Whitespace "),
|
|
format(configList[5], " Leading & Trailing Whitespace "),
|
|
format(
|
|
configList[6],
|
|
" Leading, Trailing & Internal Whitespace "),
|
|
format(configList[7], " ")});
|
|
|
|
{
|
|
ManifestCache manifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
// Correct (empty) configuration
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
emptyLocalKey, emptyCfgKeys, emptyCfgPublishers));
|
|
|
|
// load local validator key with or without manifest
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
localSigningPublicOuter, emptyCfgKeys, emptyCfgPublishers));
|
|
BEAST_EXPECT(trustedKeys->listed(localSigningPublicOuter));
|
|
|
|
manifests.applyManifest(*deserializeManifest(cfgManifest));
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
localSigningPublicOuter, emptyCfgKeys, emptyCfgPublishers));
|
|
|
|
BEAST_EXPECT(trustedKeys->listed(localMasterPublic));
|
|
BEAST_EXPECT(trustedKeys->listed(localSigningPublicOuter));
|
|
}
|
|
{
|
|
// load should add validator keys from config
|
|
ManifestCache manifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
BEAST_EXPECT(
|
|
trustedKeys->load(emptyLocalKey, cfgKeys, emptyCfgPublishers));
|
|
|
|
for (auto const& n : configList)
|
|
BEAST_EXPECT(trustedKeys->listed(n));
|
|
|
|
// load should accept Ed25519 master public keys
|
|
auto const masterNode1 = randomMasterKey();
|
|
auto const masterNode2 = randomMasterKey();
|
|
|
|
std::vector<std::string> cfgMasterKeys(
|
|
{format(masterNode1), format(masterNode2, " Comment")});
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
emptyLocalKey, cfgMasterKeys, emptyCfgPublishers));
|
|
BEAST_EXPECT(trustedKeys->listed(masterNode1));
|
|
BEAST_EXPECT(trustedKeys->listed(masterNode2));
|
|
|
|
// load should reject invalid config keys
|
|
BEAST_EXPECT(!trustedKeys->load(
|
|
emptyLocalKey, {"NotAPublicKey"}, emptyCfgPublishers));
|
|
BEAST_EXPECT(!trustedKeys->load(
|
|
emptyLocalKey,
|
|
{format(randomNode(), "!")},
|
|
emptyCfgPublishers));
|
|
|
|
// load terminates when encountering an invalid entry
|
|
auto const goodKey = randomNode();
|
|
BEAST_EXPECT(!trustedKeys->load(
|
|
emptyLocalKey,
|
|
{format(randomNode(), "!"), format(goodKey)},
|
|
emptyCfgPublishers));
|
|
BEAST_EXPECT(!trustedKeys->listed(goodKey));
|
|
}
|
|
{
|
|
// local validator key on config list
|
|
ManifestCache manifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
auto const localSigningPublic =
|
|
parseBase58<PublicKey>(TokenType::NodePublic, cfgKeys.front());
|
|
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
*localSigningPublic, cfgKeys, emptyCfgPublishers));
|
|
|
|
BEAST_EXPECT(trustedKeys->localPublicKey() == localSigningPublic);
|
|
BEAST_EXPECT(trustedKeys->listed(*localSigningPublic));
|
|
for (auto const& n : configList)
|
|
BEAST_EXPECT(trustedKeys->listed(n));
|
|
}
|
|
{
|
|
// local validator key not on config list
|
|
ManifestCache manifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
auto const localSigningPublic = randomNode();
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
localSigningPublic, cfgKeys, emptyCfgPublishers));
|
|
|
|
BEAST_EXPECT(trustedKeys->localPublicKey() == localSigningPublic);
|
|
BEAST_EXPECT(trustedKeys->listed(localSigningPublic));
|
|
for (auto const& n : configList)
|
|
BEAST_EXPECT(trustedKeys->listed(n));
|
|
}
|
|
{
|
|
// local validator key (with manifest) not on config list
|
|
ManifestCache manifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
manifests.applyManifest(*deserializeManifest(cfgManifest));
|
|
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
localSigningPublicOuter, cfgKeys, emptyCfgPublishers));
|
|
|
|
BEAST_EXPECT(trustedKeys->localPublicKey() == localMasterPublic);
|
|
BEAST_EXPECT(trustedKeys->listed(localSigningPublicOuter));
|
|
BEAST_EXPECT(trustedKeys->listed(localMasterPublic));
|
|
for (auto const& n : configList)
|
|
BEAST_EXPECT(trustedKeys->listed(n));
|
|
}
|
|
{
|
|
ManifestCache manifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
// load should reject invalid validator list signing keys
|
|
std::vector<std::string> badPublishers({"NotASigningKey"});
|
|
BEAST_EXPECT(
|
|
!trustedKeys->load(emptyLocalKey, emptyCfgKeys, badPublishers));
|
|
|
|
// load should reject validator list signing keys with invalid
|
|
// encoding
|
|
std::vector<PublicKey> keys(
|
|
{randomMasterKey(), randomMasterKey(), randomMasterKey()});
|
|
badPublishers.clear();
|
|
for (auto const& key : keys)
|
|
badPublishers.push_back(toBase58(TokenType::NodePublic, key));
|
|
|
|
BEAST_EXPECT(
|
|
!trustedKeys->load(emptyLocalKey, emptyCfgKeys, badPublishers));
|
|
for (auto const& key : keys)
|
|
BEAST_EXPECT(!trustedKeys->trustedPublisher(key));
|
|
|
|
// load should accept valid validator list publisher keys
|
|
std::vector<std::string> cfgPublishers;
|
|
for (auto const& key : keys)
|
|
cfgPublishers.push_back(strHex(key));
|
|
|
|
BEAST_EXPECT(
|
|
trustedKeys->load(emptyLocalKey, emptyCfgKeys, cfgPublishers));
|
|
for (auto const& key : keys)
|
|
BEAST_EXPECT(trustedKeys->trustedPublisher(key));
|
|
}
|
|
{
|
|
// Attempt to load a publisher key that has been revoked.
|
|
// Should fail
|
|
ManifestCache valManifests;
|
|
ManifestCache pubManifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
valManifests,
|
|
pubManifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
auto const pubRevokedSecret = randomSecretKey();
|
|
auto const pubRevokedPublic =
|
|
derivePublicKey(KeyType::ed25519, pubRevokedSecret);
|
|
auto const pubRevokedSigning = randomKeyPair(KeyType::secp256k1);
|
|
// make this manifest revoked (seq num = max)
|
|
// -- thus should not be loaded
|
|
pubManifests.applyManifest(*deserializeManifest(makeManifestString(
|
|
pubRevokedPublic,
|
|
pubRevokedSecret,
|
|
pubRevokedSigning.first,
|
|
pubRevokedSigning.second,
|
|
std::numeric_limits<std::uint32_t>::max())));
|
|
|
|
// this one is not revoked (and not in manifest cache at all.)
|
|
auto legitKey = randomMasterKey();
|
|
|
|
std::vector<std::string> cfgPublishers = {
|
|
strHex(pubRevokedPublic), strHex(legitKey)};
|
|
BEAST_EXPECT(
|
|
trustedKeys->load(emptyLocalKey, emptyCfgKeys, cfgPublishers));
|
|
|
|
BEAST_EXPECT(!trustedKeys->trustedPublisher(pubRevokedPublic));
|
|
BEAST_EXPECT(trustedKeys->trustedPublisher(legitKey));
|
|
}
|
|
}
|
|
|
|
void
|
|
testApplyLists()
|
|
{
|
|
testcase("Apply list");
|
|
using namespace std::chrono_literals;
|
|
|
|
std::string const siteUri = "testApplyList.test";
|
|
|
|
auto checkAvailable =
|
|
[this](
|
|
auto const& trustedKeys,
|
|
auto const& hexPublic,
|
|
auto const& manifest,
|
|
auto const version,
|
|
std::vector<std::pair<std::string, std::string>> const&
|
|
expected) {
|
|
const auto available = trustedKeys->getAvailable(hexPublic);
|
|
|
|
BEAST_EXPECT(!version || available);
|
|
if (available)
|
|
{
|
|
auto const& a = *available;
|
|
BEAST_EXPECT(a[jss::public_key] == hexPublic);
|
|
BEAST_EXPECT(a[jss::manifest] == manifest);
|
|
// Because multiple lists were processed, the version was
|
|
// overridden
|
|
BEAST_EXPECT(a[jss::version] == version);
|
|
if (version == 1)
|
|
{
|
|
BEAST_EXPECT(expected.size() == 1);
|
|
BEAST_EXPECT(a[jss::blob] == expected[0].first);
|
|
BEAST_EXPECT(a[jss::signature] == expected[0].second);
|
|
BEAST_EXPECT(!a.isMember(jss::blobs_v2));
|
|
}
|
|
else if (BEAST_EXPECT(a.isMember(jss::blobs_v2)))
|
|
{
|
|
BEAST_EXPECT(!a.isMember(jss::blob));
|
|
BEAST_EXPECT(!a.isMember(jss::signature));
|
|
auto const& blobs_v2 = a[jss::blobs_v2];
|
|
BEAST_EXPECT(
|
|
blobs_v2.isArray() &&
|
|
blobs_v2.size() == expected.size());
|
|
|
|
for (unsigned int i = 0; i < expected.size(); ++i)
|
|
{
|
|
BEAST_EXPECT(
|
|
blobs_v2[i][jss::blob] == expected[i].first);
|
|
BEAST_EXPECT(
|
|
blobs_v2[i][jss::signature] ==
|
|
expected[i].second);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
ManifestCache manifests;
|
|
jtx::Env env(*this);
|
|
auto& app = env.app();
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.app().timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
auto expectTrusted =
|
|
[this, &trustedKeys](std::vector<Validator> const& list) {
|
|
for (auto const& val : list)
|
|
{
|
|
BEAST_EXPECT(trustedKeys->listed(val.masterPublic));
|
|
BEAST_EXPECT(trustedKeys->listed(val.signingPublic));
|
|
}
|
|
};
|
|
|
|
auto expectUntrusted =
|
|
[this, &trustedKeys](std::vector<Validator> const& list) {
|
|
for (auto const& val : list)
|
|
{
|
|
BEAST_EXPECT(!trustedKeys->listed(val.masterPublic));
|
|
BEAST_EXPECT(!trustedKeys->listed(val.signingPublic));
|
|
}
|
|
};
|
|
|
|
auto const publisherSecret = randomSecretKey();
|
|
auto const publisherPublic =
|
|
derivePublicKey(KeyType::ed25519, publisherSecret);
|
|
const auto hexPublic =
|
|
strHex(publisherPublic.begin(), publisherPublic.end());
|
|
auto const pubSigningKeys1 = randomKeyPair(KeyType::secp256k1);
|
|
auto const manifest1 = base64_encode(makeManifestString(
|
|
publisherPublic,
|
|
publisherSecret,
|
|
pubSigningKeys1.first,
|
|
pubSigningKeys1.second,
|
|
1));
|
|
|
|
std::vector<std::string> cfgKeys1({strHex(publisherPublic)});
|
|
PublicKey emptyLocalKey;
|
|
std::vector<std::string> emptyCfgKeys;
|
|
|
|
BEAST_EXPECT(trustedKeys->load(emptyLocalKey, emptyCfgKeys, cfgKeys1));
|
|
|
|
std::map<std::size_t, std::vector<Validator>> const lists = []() {
|
|
auto constexpr listSize = 20;
|
|
auto constexpr numLists = 9;
|
|
std::map<std::size_t, std::vector<Validator>> lists;
|
|
// 1-based to correspond with the individually named blobs below.
|
|
for (auto i = 1; i <= numLists; ++i)
|
|
{
|
|
auto& list = lists[i];
|
|
list.reserve(listSize);
|
|
while (list.size() < listSize)
|
|
list.push_back(randomValidator());
|
|
}
|
|
return lists;
|
|
}();
|
|
|
|
// Attempt an expired list (fail) and a single list (succeed)
|
|
env.timeKeeper().set(env.timeKeeper().now() + 1s);
|
|
auto const version = 1;
|
|
auto const sequence1 = 1;
|
|
auto const expiredblob = makeList(
|
|
lists.at(1),
|
|
sequence1,
|
|
env.timeKeeper().now().time_since_epoch().count());
|
|
auto const expiredSig = signList(expiredblob, pubSigningKeys1);
|
|
|
|
NetClock::time_point const validUntil = env.timeKeeper().now() + 3600s;
|
|
auto const sequence2 = 2;
|
|
auto const blob2 = makeList(
|
|
lists.at(2), sequence2, validUntil.time_since_epoch().count());
|
|
auto const sig2 = signList(blob2, pubSigningKeys1);
|
|
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest1,
|
|
version,
|
|
{{expiredblob, expiredSig, {}}, {blob2, sig2, {}}},
|
|
siteUri),
|
|
publisherPublic,
|
|
ListDisposition::expired,
|
|
ListDisposition::accepted);
|
|
|
|
expectTrusted(lists.at(2));
|
|
|
|
checkAvailable(
|
|
trustedKeys, hexPublic, manifest1, version, {{blob2, sig2}});
|
|
|
|
// Do not apply future lists, but process them
|
|
auto const version2 = 2;
|
|
auto const sequence7 = 7;
|
|
auto const effective7 = validUntil - 60s;
|
|
auto const expiration7 = effective7 + 3600s;
|
|
auto const blob7 = makeList(
|
|
lists.at(7),
|
|
sequence7,
|
|
expiration7.time_since_epoch().count(),
|
|
effective7.time_since_epoch().count());
|
|
auto const sig7 = signList(blob7, pubSigningKeys1);
|
|
|
|
auto const sequence8 = 8;
|
|
auto const effective8 = expiration7 - 60s;
|
|
auto const expiration8 = effective8 + 3600s;
|
|
auto const blob8 = makeList(
|
|
lists.at(8),
|
|
sequence8,
|
|
expiration8.time_since_epoch().count(),
|
|
effective8.time_since_epoch().count());
|
|
auto const sig8 = signList(blob8, pubSigningKeys1);
|
|
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest1,
|
|
version2,
|
|
{{blob7, sig7, {}}, {blob8, sig8, {}}},
|
|
siteUri),
|
|
publisherPublic,
|
|
ListDisposition::pending,
|
|
ListDisposition::pending);
|
|
|
|
expectUntrusted(lists.at(7));
|
|
expectUntrusted(lists.at(8));
|
|
|
|
// Do not apply out-of-order future list, but process it
|
|
auto const sequence6 = 6;
|
|
auto const effective6 = effective7 - 60s;
|
|
auto const expiration6 = effective6 + 3600s;
|
|
auto const blob6 = makeList(
|
|
lists.at(6),
|
|
sequence6,
|
|
expiration6.time_since_epoch().count(),
|
|
effective6.time_since_epoch().count());
|
|
auto const sig6 = signList(blob6, pubSigningKeys1);
|
|
|
|
// Process future list that is overridden by a later list
|
|
auto const sequence6a = 5;
|
|
auto const effective6a = effective6 + 60s;
|
|
auto const expiration6a = effective6a + 3600s;
|
|
auto const blob6a = makeList(
|
|
lists.at(5),
|
|
sequence6a,
|
|
expiration6a.time_since_epoch().count(),
|
|
effective6a.time_since_epoch().count());
|
|
auto const sig6a = signList(blob6a, pubSigningKeys1);
|
|
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest1,
|
|
version,
|
|
{{blob6a, sig6a, {}}, {blob6, sig6, {}}},
|
|
siteUri),
|
|
publisherPublic,
|
|
ListDisposition::pending,
|
|
ListDisposition::pending);
|
|
|
|
expectUntrusted(lists.at(6));
|
|
expectTrusted(lists.at(2));
|
|
|
|
// Do not apply re-process lists known future sequence numbers
|
|
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest1,
|
|
version,
|
|
{{blob7, sig7, {}}, {blob6, sig6, {}}},
|
|
siteUri),
|
|
publisherPublic,
|
|
ListDisposition::known_sequence,
|
|
ListDisposition::known_sequence);
|
|
|
|
expectUntrusted(lists.at(6));
|
|
expectUntrusted(lists.at(7));
|
|
expectTrusted(lists.at(2));
|
|
|
|
// do not use list from untrusted publisher
|
|
auto const untrustedManifest = base64_encode(makeManifestString(
|
|
randomMasterKey(),
|
|
publisherSecret,
|
|
pubSigningKeys1.first,
|
|
pubSigningKeys1.second,
|
|
1));
|
|
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
untrustedManifest, version, {{blob2, sig2, {}}}, siteUri),
|
|
publisherPublic,
|
|
ListDisposition::untrusted,
|
|
ListDisposition::untrusted);
|
|
|
|
// do not use list with unhandled version
|
|
auto const badVersion = 666;
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest1, badVersion, {{blob2, sig2, {}}}, siteUri),
|
|
publisherPublic,
|
|
ListDisposition::unsupported_version,
|
|
ListDisposition::unsupported_version);
|
|
|
|
// apply list with highest sequence number
|
|
auto const sequence3 = 3;
|
|
auto const blob3 = makeList(
|
|
lists.at(3), sequence3, validUntil.time_since_epoch().count());
|
|
auto const sig3 = signList(blob3, pubSigningKeys1);
|
|
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest1, version, {{blob3, sig3, {}}}, siteUri),
|
|
publisherPublic,
|
|
ListDisposition::accepted,
|
|
ListDisposition::accepted);
|
|
|
|
expectUntrusted(lists.at(1));
|
|
expectUntrusted(lists.at(2));
|
|
expectTrusted(lists.at(3));
|
|
|
|
// Note that blob6a is not present, because it was dropped during
|
|
// processing
|
|
checkAvailable(
|
|
trustedKeys,
|
|
hexPublic,
|
|
manifest1,
|
|
2,
|
|
{{blob3, sig3}, {blob6, sig6}, {blob7, sig7}, {blob8, sig8}});
|
|
|
|
// do not re-apply lists with past or current sequence numbers
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest1,
|
|
version,
|
|
{{blob2, sig2, {}}, {blob3, sig3, {}}},
|
|
siteUri),
|
|
publisherPublic,
|
|
ListDisposition::stale,
|
|
ListDisposition::same_sequence);
|
|
|
|
// apply list with new publisher key updated by manifest. Also send some
|
|
// old lists along with the old manifest
|
|
auto const pubSigningKeys2 = randomKeyPair(KeyType::secp256k1);
|
|
auto manifest2 = base64_encode(makeManifestString(
|
|
publisherPublic,
|
|
publisherSecret,
|
|
pubSigningKeys2.first,
|
|
pubSigningKeys2.second,
|
|
2));
|
|
|
|
auto const sequence4 = 4;
|
|
auto const blob4 = makeList(
|
|
lists.at(4), sequence4, validUntil.time_since_epoch().count());
|
|
auto const sig4 = signList(blob4, pubSigningKeys2);
|
|
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest2,
|
|
version,
|
|
{{blob2, sig2, manifest1},
|
|
{blob3, sig3, manifest1},
|
|
{blob4, sig4, {}}},
|
|
siteUri),
|
|
publisherPublic,
|
|
ListDisposition::stale,
|
|
ListDisposition::accepted);
|
|
|
|
expectUntrusted(lists.at(2));
|
|
expectUntrusted(lists.at(3));
|
|
expectTrusted(lists.at(4));
|
|
|
|
checkAvailable(
|
|
trustedKeys,
|
|
hexPublic,
|
|
manifest2,
|
|
2,
|
|
{{blob4, sig4}, {blob6, sig6}, {blob7, sig7}, {blob8, sig8}});
|
|
|
|
auto const sequence5 = 5;
|
|
auto const blob5 = makeList(
|
|
lists.at(5), sequence5, validUntil.time_since_epoch().count());
|
|
auto const badSig = signList(blob5, pubSigningKeys1);
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest1, version, {{blob5, badSig, {}}}, siteUri),
|
|
publisherPublic,
|
|
ListDisposition::invalid,
|
|
ListDisposition::invalid);
|
|
|
|
expectUntrusted(lists.at(2));
|
|
expectUntrusted(lists.at(3));
|
|
expectTrusted(lists.at(4));
|
|
expectUntrusted(lists.at(5));
|
|
|
|
// Reprocess the pending list, but the signature is no longer valid
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest1,
|
|
version,
|
|
{{blob7, sig7, {}}, {blob8, sig8, {}}},
|
|
siteUri),
|
|
publisherPublic,
|
|
ListDisposition::invalid,
|
|
ListDisposition::invalid);
|
|
|
|
expectTrusted(lists.at(4));
|
|
expectUntrusted(lists.at(7));
|
|
expectUntrusted(lists.at(8));
|
|
|
|
// Automatically rotate the first pending already processed list using
|
|
// updateTrusted. Note that the timekeeper is NOT moved, so the close
|
|
// time will be ahead of the test's wall clock
|
|
trustedKeys->updateTrusted(
|
|
{},
|
|
effective6 + 1s,
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
|
|
expectUntrusted(lists.at(3));
|
|
expectTrusted(lists.at(6));
|
|
|
|
checkAvailable(
|
|
trustedKeys,
|
|
hexPublic,
|
|
manifest2,
|
|
2,
|
|
{{blob6, sig6}, {blob7, sig7}, {blob8, sig8}});
|
|
|
|
// Automatically rotate the LAST pending list using updateTrusted,
|
|
// bypassing blob7. Note that the timekeeper IS moved, so the provided
|
|
// close time will be behind the test's wall clock, and thus the wall
|
|
// clock is used.
|
|
env.timeKeeper().set(effective8);
|
|
trustedKeys->updateTrusted(
|
|
{},
|
|
effective8 + 1s,
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
|
|
expectUntrusted(lists.at(6));
|
|
expectUntrusted(lists.at(7));
|
|
expectTrusted(lists.at(8));
|
|
|
|
checkAvailable(trustedKeys, hexPublic, manifest2, 2, {{blob8, sig8}});
|
|
|
|
// resign the pending list with new key and validate it, but it's
|
|
// already valid Also try reprocessing the pending list with an
|
|
// explicit manifest
|
|
// - it is still invalid
|
|
auto const sig8_2 = signList(blob8, pubSigningKeys2);
|
|
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
manifest2,
|
|
version,
|
|
{{blob8, sig8, manifest1}, {blob8, sig8_2, {}}},
|
|
siteUri),
|
|
publisherPublic,
|
|
ListDisposition::invalid,
|
|
ListDisposition::same_sequence);
|
|
|
|
expectTrusted(lists.at(8));
|
|
|
|
checkAvailable(trustedKeys, hexPublic, manifest2, 2, {{blob8, sig8}});
|
|
|
|
// do not apply list with revoked publisher key
|
|
// applied list is removed due to revoked publisher key
|
|
auto const signingKeysMax = randomKeyPair(KeyType::secp256k1);
|
|
auto maxManifest = base64_encode(
|
|
makeRevocationString(publisherPublic, publisherSecret));
|
|
|
|
auto const sequence9 = 9;
|
|
auto const blob9 = makeList(
|
|
lists.at(9), sequence9, validUntil.time_since_epoch().count());
|
|
auto const sig9 = signList(blob9, signingKeysMax);
|
|
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
maxManifest, version, {{blob9, sig9, {}}}, siteUri),
|
|
publisherPublic,
|
|
ListDisposition::untrusted,
|
|
ListDisposition::untrusted);
|
|
|
|
BEAST_EXPECT(!trustedKeys->trustedPublisher(publisherPublic));
|
|
for (auto const& [num, list] : lists)
|
|
{
|
|
(void)num;
|
|
expectUntrusted(list);
|
|
}
|
|
|
|
checkAvailable(trustedKeys, hexPublic, manifest2, 0, {});
|
|
}
|
|
|
|
void
|
|
testGetAvailable()
|
|
{
|
|
testcase("GetAvailable");
|
|
using namespace std::chrono_literals;
|
|
|
|
std::string const siteUri = "testApplyList.test";
|
|
|
|
ManifestCache manifests;
|
|
jtx::Env env(*this);
|
|
auto& app = env.app();
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.app().timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
auto const publisherSecret = randomSecretKey();
|
|
auto const publisherPublic =
|
|
derivePublicKey(KeyType::ed25519, publisherSecret);
|
|
const auto hexPublic =
|
|
strHex(publisherPublic.begin(), publisherPublic.end());
|
|
auto const pubSigningKeys1 = randomKeyPair(KeyType::secp256k1);
|
|
auto const manifest = base64_encode(makeManifestString(
|
|
publisherPublic,
|
|
publisherSecret,
|
|
pubSigningKeys1.first,
|
|
pubSigningKeys1.second,
|
|
1));
|
|
|
|
std::vector<std::string> cfgKeys1({strHex(publisherPublic)});
|
|
PublicKey emptyLocalKey;
|
|
std::vector<std::string> emptyCfgKeys;
|
|
|
|
BEAST_EXPECT(trustedKeys->load(emptyLocalKey, emptyCfgKeys, cfgKeys1));
|
|
|
|
std::vector<Validator> const list = []() {
|
|
auto constexpr listSize = 20;
|
|
std::vector<Validator> list;
|
|
list.reserve(listSize);
|
|
while (list.size() < listSize)
|
|
list.push_back(randomValidator());
|
|
return list;
|
|
}();
|
|
|
|
// Process a list
|
|
env.timeKeeper().set(env.timeKeeper().now() + 1s);
|
|
NetClock::time_point const validUntil = env.timeKeeper().now() + 3600s;
|
|
auto const blob =
|
|
makeList(list, 1, validUntil.time_since_epoch().count());
|
|
auto const sig = signList(blob, pubSigningKeys1);
|
|
|
|
{
|
|
// list unavailable
|
|
auto const available = trustedKeys->getAvailable(hexPublic);
|
|
BEAST_EXPECT(!available);
|
|
}
|
|
|
|
BEAST_EXPECT(
|
|
trustedKeys->applyLists(manifest, 1, {{blob, sig, {}}}, siteUri)
|
|
.bestDisposition() == ListDisposition::accepted);
|
|
|
|
{
|
|
// invalid public key
|
|
auto const available =
|
|
trustedKeys->getAvailable(hexPublic + "invalid", 1);
|
|
BEAST_EXPECT(!available);
|
|
}
|
|
|
|
{
|
|
// unknown public key
|
|
auto const badSecret = randomSecretKey();
|
|
auto const badPublic = derivePublicKey(KeyType::ed25519, badSecret);
|
|
const auto hexBad = strHex(badPublic.begin(), badPublic.end());
|
|
|
|
auto const available = trustedKeys->getAvailable(hexBad, 1);
|
|
BEAST_EXPECT(!available);
|
|
}
|
|
{
|
|
// bad version 0
|
|
auto const available = trustedKeys->getAvailable(hexPublic, 0);
|
|
if (BEAST_EXPECT(available))
|
|
{
|
|
auto const& a = *available;
|
|
BEAST_EXPECT(!a);
|
|
}
|
|
}
|
|
{
|
|
// bad version 3
|
|
auto const available = trustedKeys->getAvailable(hexPublic, 3);
|
|
if (BEAST_EXPECT(available))
|
|
{
|
|
auto const& a = *available;
|
|
BEAST_EXPECT(!a);
|
|
}
|
|
}
|
|
{
|
|
// version 1
|
|
auto const available = trustedKeys->getAvailable(hexPublic, 1);
|
|
if (BEAST_EXPECT(available))
|
|
{
|
|
auto const& a = *available;
|
|
BEAST_EXPECT(a[jss::public_key] == hexPublic);
|
|
BEAST_EXPECT(a[jss::manifest] == manifest);
|
|
BEAST_EXPECT(a[jss::version] == 1);
|
|
|
|
BEAST_EXPECT(a[jss::blob] == blob);
|
|
BEAST_EXPECT(a[jss::signature] == sig);
|
|
BEAST_EXPECT(!a.isMember(jss::blobs_v2));
|
|
}
|
|
}
|
|
|
|
{
|
|
// version 2
|
|
auto const available = trustedKeys->getAvailable(hexPublic, 2);
|
|
if (BEAST_EXPECT(available))
|
|
{
|
|
auto const& a = *available;
|
|
BEAST_EXPECT(a[jss::public_key] == hexPublic);
|
|
BEAST_EXPECT(a[jss::manifest] == manifest);
|
|
BEAST_EXPECT(a[jss::version] == 2);
|
|
|
|
if (BEAST_EXPECT(a.isMember(jss::blobs_v2)))
|
|
{
|
|
BEAST_EXPECT(!a.isMember(jss::blob));
|
|
BEAST_EXPECT(!a.isMember(jss::signature));
|
|
auto const& blobs_v2 = a[jss::blobs_v2];
|
|
BEAST_EXPECT(blobs_v2.isArray() && blobs_v2.size() == 1);
|
|
|
|
BEAST_EXPECT(blobs_v2[0u][jss::blob] == blob);
|
|
BEAST_EXPECT(blobs_v2[0u][jss::signature] == sig);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testUpdateTrusted()
|
|
{
|
|
testcase("Update trusted");
|
|
|
|
std::string const siteUri = "testUpdateTrusted.test";
|
|
|
|
PublicKey emptyLocalKeyOuter;
|
|
ManifestCache manifestsOuter;
|
|
jtx::Env env(*this);
|
|
auto& app = env.app();
|
|
auto trustedKeysOuter = std::make_unique<ValidatorList>(
|
|
manifestsOuter,
|
|
manifestsOuter,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
std::vector<std::string> cfgPublishersOuter;
|
|
hash_set<NodeID> activeValidatorsOuter;
|
|
|
|
std::size_t const maxKeys = 40;
|
|
{
|
|
std::vector<std::string> cfgKeys;
|
|
cfgKeys.reserve(maxKeys);
|
|
hash_set<NodeID> unseenValidators;
|
|
|
|
while (cfgKeys.size() != maxKeys)
|
|
{
|
|
auto const valKey = randomNode();
|
|
cfgKeys.push_back(toBase58(TokenType::NodePublic, valKey));
|
|
if (cfgKeys.size() <= maxKeys - 5)
|
|
activeValidatorsOuter.emplace(calcNodeID(valKey));
|
|
else
|
|
unseenValidators.emplace(calcNodeID(valKey));
|
|
}
|
|
|
|
BEAST_EXPECT(trustedKeysOuter->load(
|
|
emptyLocalKeyOuter, cfgKeys, cfgPublishersOuter));
|
|
|
|
// updateTrusted should make all configured validators trusted
|
|
// even if they are not active/seen
|
|
TrustChanges changes = trustedKeysOuter->updateTrusted(
|
|
activeValidatorsOuter,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
|
|
for (auto const& val : unseenValidators)
|
|
activeValidatorsOuter.emplace(val);
|
|
|
|
BEAST_EXPECT(changes.added == activeValidatorsOuter);
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
BEAST_EXPECT(
|
|
trustedKeysOuter->quorum() == std::ceil(cfgKeys.size() * 0.8f));
|
|
for (auto const& val : cfgKeys)
|
|
{
|
|
if (auto const valKey =
|
|
parseBase58<PublicKey>(TokenType::NodePublic, val))
|
|
{
|
|
BEAST_EXPECT(trustedKeysOuter->listed(*valKey));
|
|
BEAST_EXPECT(trustedKeysOuter->trusted(*valKey));
|
|
}
|
|
else
|
|
fail();
|
|
}
|
|
|
|
changes = trustedKeysOuter->updateTrusted(
|
|
activeValidatorsOuter,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.added.empty());
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
BEAST_EXPECT(
|
|
trustedKeysOuter->quorum() == std::ceil(cfgKeys.size() * 0.8f));
|
|
}
|
|
{
|
|
// update with manifests
|
|
auto const masterPrivate = randomSecretKey();
|
|
auto const masterPublic =
|
|
derivePublicKey(KeyType::ed25519, masterPrivate);
|
|
|
|
std::vector<std::string> cfgKeys(
|
|
{toBase58(TokenType::NodePublic, masterPublic)});
|
|
|
|
BEAST_EXPECT(trustedKeysOuter->load(
|
|
emptyLocalKeyOuter, cfgKeys, cfgPublishersOuter));
|
|
|
|
auto const signingKeys1 = randomKeyPair(KeyType::secp256k1);
|
|
auto const signingPublic1 = signingKeys1.first;
|
|
activeValidatorsOuter.emplace(calcNodeID(masterPublic));
|
|
|
|
// Should not trust ephemeral signing key if there is no manifest
|
|
TrustChanges changes = trustedKeysOuter->updateTrusted(
|
|
activeValidatorsOuter,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.added == asNodeIDs({masterPublic}));
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
BEAST_EXPECT(
|
|
trustedKeysOuter->quorum() == std::ceil((maxKeys + 1) * 0.8f));
|
|
BEAST_EXPECT(trustedKeysOuter->listed(masterPublic));
|
|
BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic));
|
|
BEAST_EXPECT(!trustedKeysOuter->listed(signingPublic1));
|
|
BEAST_EXPECT(!trustedKeysOuter->trusted(signingPublic1));
|
|
|
|
// Should trust the ephemeral signing key from the applied manifest
|
|
auto m1 = deserializeManifest(makeManifestString(
|
|
masterPublic,
|
|
masterPrivate,
|
|
signingPublic1,
|
|
signingKeys1.second,
|
|
1));
|
|
|
|
BEAST_EXPECT(
|
|
manifestsOuter.applyManifest(std::move(*m1)) ==
|
|
ManifestDisposition::accepted);
|
|
BEAST_EXPECT(trustedKeysOuter->listed(masterPublic));
|
|
BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic));
|
|
BEAST_EXPECT(trustedKeysOuter->listed(signingPublic1));
|
|
BEAST_EXPECT(trustedKeysOuter->trusted(signingPublic1));
|
|
|
|
// Should only trust the ephemeral signing key
|
|
// from the newest applied manifest
|
|
auto const signingKeys2 = randomKeyPair(KeyType::secp256k1);
|
|
auto const signingPublic2 = signingKeys2.first;
|
|
auto m2 = deserializeManifest(makeManifestString(
|
|
masterPublic,
|
|
masterPrivate,
|
|
signingPublic2,
|
|
signingKeys2.second,
|
|
2));
|
|
BEAST_EXPECT(
|
|
manifestsOuter.applyManifest(std::move(*m2)) ==
|
|
ManifestDisposition::accepted);
|
|
BEAST_EXPECT(trustedKeysOuter->listed(masterPublic));
|
|
BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic));
|
|
BEAST_EXPECT(trustedKeysOuter->listed(signingPublic2));
|
|
BEAST_EXPECT(trustedKeysOuter->trusted(signingPublic2));
|
|
BEAST_EXPECT(!trustedKeysOuter->listed(signingPublic1));
|
|
BEAST_EXPECT(!trustedKeysOuter->trusted(signingPublic1));
|
|
|
|
// Should not trust keys from revoked master public key
|
|
auto const signingKeysMax = randomKeyPair(KeyType::secp256k1);
|
|
auto const signingPublicMax = signingKeysMax.first;
|
|
activeValidatorsOuter.emplace(calcNodeID(signingPublicMax));
|
|
auto mMax = deserializeManifest(
|
|
makeRevocationString(masterPublic, masterPrivate));
|
|
|
|
BEAST_EXPECT(mMax->revoked());
|
|
BEAST_EXPECT(
|
|
manifestsOuter.applyManifest(std::move(*mMax)) ==
|
|
ManifestDisposition::accepted);
|
|
BEAST_EXPECT(
|
|
manifestsOuter.getSigningKey(masterPublic) == masterPublic);
|
|
BEAST_EXPECT(manifestsOuter.revoked(masterPublic));
|
|
|
|
// Revoked key remains trusted until list is updated
|
|
BEAST_EXPECT(trustedKeysOuter->listed(masterPublic));
|
|
BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic));
|
|
|
|
changes = trustedKeysOuter->updateTrusted(
|
|
activeValidatorsOuter,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.removed == asNodeIDs({masterPublic}));
|
|
BEAST_EXPECT(changes.added.empty());
|
|
BEAST_EXPECT(
|
|
trustedKeysOuter->quorum() == std::ceil(maxKeys * 0.8f));
|
|
BEAST_EXPECT(trustedKeysOuter->listed(masterPublic));
|
|
BEAST_EXPECT(!trustedKeysOuter->trusted(masterPublic));
|
|
BEAST_EXPECT(!trustedKeysOuter->listed(signingPublicMax));
|
|
BEAST_EXPECT(!trustedKeysOuter->trusted(signingPublicMax));
|
|
BEAST_EXPECT(!trustedKeysOuter->listed(signingPublic2));
|
|
BEAST_EXPECT(!trustedKeysOuter->trusted(signingPublic2));
|
|
BEAST_EXPECT(!trustedKeysOuter->listed(signingPublic1));
|
|
BEAST_EXPECT(!trustedKeysOuter->trusted(signingPublic1));
|
|
}
|
|
{
|
|
// Make quorum unattainable if lists from any publishers are
|
|
// unavailable
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifestsOuter,
|
|
manifestsOuter,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
auto const publisherSecret = randomSecretKey();
|
|
auto const publisherPublic =
|
|
derivePublicKey(KeyType::ed25519, publisherSecret);
|
|
|
|
std::vector<std::string> cfgPublishers({strHex(publisherPublic)});
|
|
std::vector<std::string> emptyCfgKeys;
|
|
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
emptyLocalKeyOuter, emptyCfgKeys, cfgPublishers));
|
|
|
|
TrustChanges changes = trustedKeys->updateTrusted(
|
|
activeValidatorsOuter,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
BEAST_EXPECT(changes.added.empty());
|
|
BEAST_EXPECT(
|
|
trustedKeys->quorum() ==
|
|
std::numeric_limits<std::size_t>::max());
|
|
}
|
|
{
|
|
// Should use custom minimum quorum
|
|
std::size_t const minQuorum = 1;
|
|
ManifestCache manifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal,
|
|
minQuorum);
|
|
|
|
std::size_t n = 10;
|
|
std::vector<std::string> cfgKeys;
|
|
cfgKeys.reserve(n);
|
|
hash_set<NodeID> expectedTrusted;
|
|
hash_set<NodeID> activeValidators;
|
|
NodeID toBeSeen;
|
|
|
|
while (cfgKeys.size() < n)
|
|
{
|
|
auto const valKey = randomNode();
|
|
cfgKeys.push_back(toBase58(TokenType::NodePublic, valKey));
|
|
expectedTrusted.emplace(calcNodeID(valKey));
|
|
if (cfgKeys.size() < std::ceil(n * 0.8f))
|
|
activeValidators.emplace(calcNodeID(valKey));
|
|
else if (cfgKeys.size() < std::ceil(n * 0.8f))
|
|
toBeSeen = calcNodeID(valKey);
|
|
}
|
|
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
emptyLocalKeyOuter, cfgKeys, cfgPublishersOuter));
|
|
|
|
TrustChanges changes = trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
BEAST_EXPECT(changes.added == expectedTrusted);
|
|
BEAST_EXPECT(trustedKeys->quorum() == minQuorum);
|
|
|
|
// Use normal quorum when seen validators >= quorum
|
|
activeValidators.emplace(toBeSeen);
|
|
changes = trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
BEAST_EXPECT(changes.added.empty());
|
|
BEAST_EXPECT(trustedKeys->quorum() == std::ceil(n * 0.8f));
|
|
}
|
|
{
|
|
// Remove expired published list
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifestsOuter,
|
|
manifestsOuter,
|
|
env.app().timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
PublicKey emptyLocalKey;
|
|
std::vector<std::string> emptyCfgKeys;
|
|
auto const publisherKeys = randomKeyPair(KeyType::secp256k1);
|
|
auto const pubSigningKeys = randomKeyPair(KeyType::secp256k1);
|
|
auto const manifest = base64_encode(makeManifestString(
|
|
publisherKeys.first,
|
|
publisherKeys.second,
|
|
pubSigningKeys.first,
|
|
pubSigningKeys.second,
|
|
1));
|
|
|
|
std::vector<std::string> cfgKeys({strHex(publisherKeys.first)});
|
|
|
|
BEAST_EXPECT(
|
|
trustedKeys->load(emptyLocalKey, emptyCfgKeys, cfgKeys));
|
|
|
|
std::vector<Validator> list({randomValidator(), randomValidator()});
|
|
hash_set<NodeID> activeValidators(
|
|
asNodeIDs({list[0].masterPublic, list[1].masterPublic}));
|
|
|
|
// do not apply expired list
|
|
auto const version = 1;
|
|
auto const sequence = 1;
|
|
using namespace std::chrono_literals;
|
|
NetClock::time_point const validUntil =
|
|
env.timeKeeper().now() + 60s;
|
|
auto const blob =
|
|
makeList(list, sequence, validUntil.time_since_epoch().count());
|
|
auto const sig = signList(blob, pubSigningKeys);
|
|
|
|
BEAST_EXPECT(
|
|
ListDisposition::accepted ==
|
|
trustedKeys
|
|
->applyLists(manifest, version, {{blob, sig, {}}}, siteUri)
|
|
.bestDisposition());
|
|
|
|
TrustChanges changes = trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
BEAST_EXPECT(changes.added == activeValidators);
|
|
for (Validator const& val : list)
|
|
{
|
|
BEAST_EXPECT(trustedKeys->trusted(val.masterPublic));
|
|
BEAST_EXPECT(trustedKeys->trusted(val.signingPublic));
|
|
}
|
|
BEAST_EXPECT(trustedKeys->quorum() == 2);
|
|
|
|
env.timeKeeper().set(validUntil);
|
|
changes = trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.removed == activeValidators);
|
|
BEAST_EXPECT(changes.added.empty());
|
|
BEAST_EXPECT(!trustedKeys->trusted(list[0].masterPublic));
|
|
BEAST_EXPECT(!trustedKeys->trusted(list[1].masterPublic));
|
|
BEAST_EXPECT(
|
|
trustedKeys->quorum() ==
|
|
std::numeric_limits<std::size_t>::max());
|
|
|
|
// (Re)trust validators from new valid list
|
|
std::vector<Validator> list2({list[0], randomValidator()});
|
|
activeValidators.insert(calcNodeID(list2[1].masterPublic));
|
|
auto const sequence2 = 2;
|
|
NetClock::time_point const expiration2 =
|
|
env.timeKeeper().now() + 60s;
|
|
auto const blob2 = makeList(
|
|
list2, sequence2, expiration2.time_since_epoch().count());
|
|
auto const sig2 = signList(blob2, pubSigningKeys);
|
|
|
|
BEAST_EXPECT(
|
|
ListDisposition::accepted ==
|
|
trustedKeys
|
|
->applyLists(
|
|
manifest, version, {{blob2, sig2, {}}}, siteUri)
|
|
.bestDisposition());
|
|
|
|
changes = trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
BEAST_EXPECT(
|
|
changes.added ==
|
|
asNodeIDs({list2[0].masterPublic, list2[1].masterPublic}));
|
|
for (Validator const& val : list2)
|
|
{
|
|
BEAST_EXPECT(trustedKeys->trusted(val.masterPublic));
|
|
BEAST_EXPECT(trustedKeys->trusted(val.signingPublic));
|
|
}
|
|
BEAST_EXPECT(!trustedKeys->trusted(list[1].masterPublic));
|
|
BEAST_EXPECT(!trustedKeys->trusted(list[1].signingPublic));
|
|
BEAST_EXPECT(trustedKeys->quorum() == 2);
|
|
}
|
|
{
|
|
// Test 1-9 configured validators
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifestsOuter,
|
|
manifestsOuter,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
std::vector<std::string> cfgPublishers;
|
|
hash_set<NodeID> activeValidators;
|
|
hash_set<PublicKey> activeKeys;
|
|
|
|
std::vector<std::string> cfgKeys;
|
|
cfgKeys.reserve(9);
|
|
|
|
while (cfgKeys.size() < cfgKeys.capacity())
|
|
{
|
|
auto const valKey = randomNode();
|
|
cfgKeys.push_back(toBase58(TokenType::NodePublic, valKey));
|
|
activeValidators.emplace(calcNodeID(valKey));
|
|
activeKeys.emplace(valKey);
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
emptyLocalKeyOuter, cfgKeys, cfgPublishers));
|
|
TrustChanges changes = trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
BEAST_EXPECT(changes.added == asNodeIDs({valKey}));
|
|
BEAST_EXPECT(
|
|
trustedKeys->quorum() == std::ceil(cfgKeys.size() * 0.8f));
|
|
for (auto const& key : activeKeys)
|
|
BEAST_EXPECT(trustedKeys->trusted(key));
|
|
}
|
|
}
|
|
{
|
|
// Test 2-9 configured validators as validator
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifestsOuter,
|
|
manifestsOuter,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
auto const localKey = randomNode();
|
|
std::vector<std::string> cfgPublishers;
|
|
hash_set<NodeID> activeValidators;
|
|
hash_set<PublicKey> activeKeys;
|
|
std::vector<std::string> cfgKeys{
|
|
toBase58(TokenType::NodePublic, localKey)};
|
|
cfgKeys.reserve(9);
|
|
|
|
while (cfgKeys.size() < cfgKeys.capacity())
|
|
{
|
|
auto const valKey = randomNode();
|
|
cfgKeys.push_back(toBase58(TokenType::NodePublic, valKey));
|
|
activeValidators.emplace(calcNodeID(valKey));
|
|
activeKeys.emplace(valKey);
|
|
|
|
BEAST_EXPECT(
|
|
trustedKeys->load(localKey, cfgKeys, cfgPublishers));
|
|
TrustChanges changes = trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
if (cfgKeys.size() > 2)
|
|
BEAST_EXPECT(changes.added == asNodeIDs({valKey}));
|
|
else
|
|
BEAST_EXPECT(
|
|
changes.added == asNodeIDs({localKey, valKey}));
|
|
|
|
BEAST_EXPECT(
|
|
trustedKeys->quorum() == std::ceil(cfgKeys.size() * 0.8f));
|
|
|
|
for (auto const& key : activeKeys)
|
|
BEAST_EXPECT(trustedKeys->trusted(key));
|
|
}
|
|
}
|
|
{
|
|
// Trusted set should include all validators from multiple lists
|
|
ManifestCache manifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
hash_set<NodeID> activeValidators;
|
|
std::vector<Validator> valKeys;
|
|
valKeys.reserve(maxKeys);
|
|
|
|
while (valKeys.size() != maxKeys)
|
|
{
|
|
valKeys.push_back(randomValidator());
|
|
activeValidators.emplace(
|
|
calcNodeID(valKeys.back().masterPublic));
|
|
}
|
|
|
|
auto addPublishedList = [this,
|
|
&env,
|
|
&trustedKeys,
|
|
&valKeys,
|
|
&siteUri]() {
|
|
auto const publisherSecret = randomSecretKey();
|
|
auto const publisherPublic =
|
|
derivePublicKey(KeyType::ed25519, publisherSecret);
|
|
auto const pubSigningKeys = randomKeyPair(KeyType::secp256k1);
|
|
auto const manifest = base64_encode(makeManifestString(
|
|
publisherPublic,
|
|
publisherSecret,
|
|
pubSigningKeys.first,
|
|
pubSigningKeys.second,
|
|
1));
|
|
|
|
std::vector<std::string> cfgPublishers(
|
|
{strHex(publisherPublic)});
|
|
PublicKey emptyLocalKey;
|
|
std::vector<std::string> emptyCfgKeys;
|
|
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
emptyLocalKey, emptyCfgKeys, cfgPublishers));
|
|
|
|
auto const version = 1;
|
|
auto const sequence = 1;
|
|
using namespace std::chrono_literals;
|
|
NetClock::time_point const validUntil =
|
|
env.timeKeeper().now() + 3600s;
|
|
auto const blob = makeList(
|
|
valKeys, sequence, validUntil.time_since_epoch().count());
|
|
auto const sig = signList(blob, pubSigningKeys);
|
|
|
|
BEAST_EXPECT(
|
|
ListDisposition::accepted ==
|
|
trustedKeys
|
|
->applyLists(
|
|
manifest, version, {{blob, sig, {}}}, siteUri)
|
|
.bestDisposition());
|
|
};
|
|
|
|
// Apply multiple published lists
|
|
for (auto i = 0; i < 3; ++i)
|
|
addPublishedList();
|
|
|
|
TrustChanges changes = trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
|
|
BEAST_EXPECT(
|
|
trustedKeys->quorum() == std::ceil(valKeys.size() * 0.8f));
|
|
|
|
hash_set<NodeID> added;
|
|
for (auto const& val : valKeys)
|
|
{
|
|
BEAST_EXPECT(trustedKeys->trusted(val.masterPublic));
|
|
added.insert(calcNodeID(val.masterPublic));
|
|
}
|
|
BEAST_EXPECT(changes.added == added);
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
}
|
|
}
|
|
|
|
void
|
|
testExpires()
|
|
{
|
|
testcase("Expires");
|
|
|
|
std::string const siteUri = "testExpires.test";
|
|
|
|
jtx::Env env(*this);
|
|
auto& app = env.app();
|
|
|
|
auto toStr = [](PublicKey const& publicKey) {
|
|
return toBase58(TokenType::NodePublic, publicKey);
|
|
};
|
|
|
|
// Config listed keys
|
|
{
|
|
ManifestCache manifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
// Empty list has no expiration
|
|
BEAST_EXPECT(trustedKeys->expires() == std::nullopt);
|
|
|
|
// Config listed keys have maximum expiry
|
|
PublicKey emptyLocalKey;
|
|
PublicKey localCfgListed = randomNode();
|
|
trustedKeys->load(emptyLocalKey, {toStr(localCfgListed)}, {});
|
|
BEAST_EXPECT(
|
|
trustedKeys->expires() &&
|
|
trustedKeys->expires().value() == NetClock::time_point::max());
|
|
BEAST_EXPECT(trustedKeys->listed(localCfgListed));
|
|
}
|
|
|
|
// Published keys with expirations
|
|
{
|
|
ManifestCache manifests;
|
|
auto trustedKeys = std::make_unique<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.app().timeKeeper(),
|
|
app.config().legacy("database_path"),
|
|
env.journal);
|
|
|
|
std::vector<Validator> validators = {randomValidator()};
|
|
hash_set<NodeID> activeValidators;
|
|
for (Validator const& val : validators)
|
|
activeValidators.insert(calcNodeID(val.masterPublic));
|
|
// Store prepared list data to control when it is applied
|
|
struct PreparedList
|
|
{
|
|
PublicKey publisherPublic;
|
|
std::string manifest;
|
|
std::vector<ValidatorBlobInfo> blobs;
|
|
int version;
|
|
std::vector<NetClock::time_point> expirations;
|
|
};
|
|
|
|
using namespace std::chrono_literals;
|
|
auto addPublishedList = [this, &env, &trustedKeys, &validators]() {
|
|
auto const publisherSecret = randomSecretKey();
|
|
auto const publisherPublic =
|
|
derivePublicKey(KeyType::ed25519, publisherSecret);
|
|
auto const pubSigningKeys = randomKeyPair(KeyType::secp256k1);
|
|
auto const manifest = base64_encode(makeManifestString(
|
|
publisherPublic,
|
|
publisherSecret,
|
|
pubSigningKeys.first,
|
|
pubSigningKeys.second,
|
|
1));
|
|
|
|
std::vector<std::string> cfgPublishers(
|
|
{strHex(publisherPublic)});
|
|
PublicKey emptyLocalKey;
|
|
std::vector<std::string> emptyCfgKeys;
|
|
|
|
BEAST_EXPECT(trustedKeys->load(
|
|
emptyLocalKey, emptyCfgKeys, cfgPublishers));
|
|
|
|
auto const version = 2;
|
|
auto const sequence1 = 1;
|
|
NetClock::time_point const expiration1 =
|
|
env.timeKeeper().now() + 1800s;
|
|
auto const blob1 = makeList(
|
|
validators,
|
|
sequence1,
|
|
expiration1.time_since_epoch().count());
|
|
auto const sig1 = signList(blob1, pubSigningKeys);
|
|
|
|
NetClock::time_point const effective2 = expiration1 - 300s;
|
|
NetClock::time_point const expiration2 = effective2 + 1800s;
|
|
auto const sequence2 = 2;
|
|
auto const blob2 = makeList(
|
|
validators,
|
|
sequence2,
|
|
expiration2.time_since_epoch().count(),
|
|
effective2.time_since_epoch().count());
|
|
auto const sig2 = signList(blob2, pubSigningKeys);
|
|
|
|
return PreparedList{
|
|
publisherPublic,
|
|
manifest,
|
|
{{blob1, sig1, {}}, {blob2, sig2, {}}},
|
|
version,
|
|
{expiration1, expiration2}};
|
|
};
|
|
|
|
// Configure two publishers and prepare 2 lists
|
|
PreparedList prep1 = addPublishedList();
|
|
env.timeKeeper().set(env.timeKeeper().now() + 200s);
|
|
PreparedList prep2 = addPublishedList();
|
|
|
|
// Initially, no list has been published, so no known expiration
|
|
BEAST_EXPECT(trustedKeys->expires() == std::nullopt);
|
|
|
|
// Apply first list
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
prep1.manifest, prep1.version, prep1.blobs, siteUri),
|
|
prep1.publisherPublic,
|
|
ListDisposition::pending,
|
|
ListDisposition::accepted);
|
|
|
|
// One list still hasn't published, so expiration is still
|
|
// unknown
|
|
BEAST_EXPECT(trustedKeys->expires() == std::nullopt);
|
|
|
|
// Apply second list
|
|
checkResult(
|
|
trustedKeys->applyLists(
|
|
prep2.manifest, prep2.version, prep2.blobs, siteUri),
|
|
prep2.publisherPublic,
|
|
ListDisposition::pending,
|
|
ListDisposition::accepted);
|
|
// We now have loaded both lists, so expiration is known
|
|
BEAST_EXPECT(
|
|
trustedKeys->expires() &&
|
|
trustedKeys->expires().value() == prep1.expirations.back());
|
|
|
|
// Advance past the first list's LAST validFrom date. It remains
|
|
// the earliest validUntil, while rotating in the second list
|
|
{
|
|
env.timeKeeper().set(prep1.expirations.front() - 1s);
|
|
auto changes = trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(
|
|
trustedKeys->expires() &&
|
|
trustedKeys->expires().value() == prep1.expirations.back());
|
|
BEAST_EXPECT(!changes.added.empty());
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
}
|
|
|
|
// Advance past the first list's LAST validUntil, but it remains
|
|
// the earliest validUntil, while being invalidated
|
|
{
|
|
env.timeKeeper().set(prep1.expirations.back() + 1s);
|
|
auto changes = trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(
|
|
trustedKeys->expires() &&
|
|
trustedKeys->expires().value() == prep1.expirations.back());
|
|
BEAST_EXPECT(changes.added.empty());
|
|
BEAST_EXPECT(changes.removed.empty());
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testNegativeUNL()
|
|
{
|
|
testcase("NegativeUNL");
|
|
jtx::Env env(*this);
|
|
PublicKey emptyLocalKey;
|
|
ManifestCache manifests;
|
|
|
|
auto createValidatorList =
|
|
[&](std::uint32_t vlSize,
|
|
std::optional<std::size_t> minimumQuorum = {})
|
|
-> std::shared_ptr<ValidatorList> {
|
|
auto trustedKeys = std::make_shared<ValidatorList>(
|
|
manifests,
|
|
manifests,
|
|
env.timeKeeper(),
|
|
env.app().config().legacy("database_path"),
|
|
env.journal,
|
|
minimumQuorum);
|
|
|
|
std::vector<std::string> cfgPublishers;
|
|
std::vector<std::string> cfgKeys;
|
|
hash_set<NodeID> activeValidators;
|
|
cfgKeys.reserve(vlSize);
|
|
while (cfgKeys.size() < cfgKeys.capacity())
|
|
{
|
|
auto const valKey = randomNode();
|
|
cfgKeys.push_back(toBase58(TokenType::NodePublic, valKey));
|
|
activeValidators.emplace(calcNodeID(valKey));
|
|
}
|
|
if (trustedKeys->load(emptyLocalKey, cfgKeys, cfgPublishers))
|
|
{
|
|
trustedKeys->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
if (trustedKeys->quorum() == std::ceil(cfgKeys.size() * 0.8f))
|
|
return trustedKeys;
|
|
}
|
|
return nullptr;
|
|
};
|
|
|
|
/*
|
|
* Test NegativeUNL
|
|
* == Combinations ==
|
|
* -- UNL size: 34, 35, 57
|
|
* -- nUNL size: 0%, 20%, 30%, 50%
|
|
*
|
|
* == with UNL size 60
|
|
* -- set == get,
|
|
* -- check quorum, with nUNL size: 0, 12, 30, 18
|
|
* -- nUNL overlap: |nUNL - UNL| = 5, with nUNL size: 18
|
|
* -- with command line minimumQuorum = 50%,
|
|
* seen_reliable affected by nUNL
|
|
*/
|
|
|
|
{
|
|
hash_set<NodeID> activeValidators;
|
|
//== Combinations ==
|
|
std::array<std::uint32_t, 4> unlSizes = {34, 35, 39, 60};
|
|
std::array<std::uint32_t, 4> nUnlPercent = {0, 20, 30, 50};
|
|
for (auto us : unlSizes)
|
|
{
|
|
for (auto np : nUnlPercent)
|
|
{
|
|
auto validators = createValidatorList(us);
|
|
BEAST_EXPECT(validators);
|
|
if (validators)
|
|
{
|
|
std::uint32_t nUnlSize = us * np / 100;
|
|
auto unl = validators->getTrustedMasterKeys();
|
|
hash_set<PublicKey> nUnl;
|
|
auto it = unl.begin();
|
|
for (std::uint32_t i = 0; i < nUnlSize; ++i)
|
|
{
|
|
nUnl.insert(*it);
|
|
++it;
|
|
}
|
|
validators->setNegativeUNL(nUnl);
|
|
validators->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(
|
|
validators->quorum() ==
|
|
static_cast<std::size_t>(std::ceil(
|
|
std::max((us - nUnlSize) * 0.8f, us * 0.6f))));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
//== with UNL size 60
|
|
auto validators = createValidatorList(60);
|
|
BEAST_EXPECT(validators);
|
|
if (validators)
|
|
{
|
|
hash_set<NodeID> activeValidators;
|
|
auto unl = validators->getTrustedMasterKeys();
|
|
BEAST_EXPECT(unl.size() == 60);
|
|
{
|
|
//-- set == get,
|
|
//-- check quorum, with nUNL size: 0, 30, 18, 12
|
|
auto nUnlChange = [&](std::uint32_t nUnlSize,
|
|
std::uint32_t quorum) -> bool {
|
|
hash_set<PublicKey> nUnl;
|
|
auto it = unl.begin();
|
|
for (std::uint32_t i = 0; i < nUnlSize; ++i)
|
|
{
|
|
nUnl.insert(*it);
|
|
++it;
|
|
}
|
|
validators->setNegativeUNL(nUnl);
|
|
auto nUnl_temp = validators->getNegativeUNL();
|
|
if (nUnl_temp.size() == nUnl.size())
|
|
{
|
|
for (auto& n : nUnl_temp)
|
|
{
|
|
if (nUnl.find(n) == nUnl.end())
|
|
return false;
|
|
}
|
|
validators->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
return validators->quorum() == quorum;
|
|
}
|
|
return false;
|
|
};
|
|
BEAST_EXPECT(nUnlChange(0, 48));
|
|
BEAST_EXPECT(nUnlChange(30, 36));
|
|
BEAST_EXPECT(nUnlChange(18, 36));
|
|
BEAST_EXPECT(nUnlChange(12, 39));
|
|
}
|
|
|
|
{
|
|
// nUNL overlap: |nUNL - UNL| = 5, with nUNL size:
|
|
// 18
|
|
auto nUnl = validators->getNegativeUNL();
|
|
BEAST_EXPECT(nUnl.size() == 12);
|
|
std::size_t ss = 33;
|
|
std::vector<uint8_t> data(ss, 0);
|
|
data[0] = 0xED;
|
|
for (int i = 0; i < 6; ++i)
|
|
{
|
|
Slice s(data.data(), ss);
|
|
data[1]++;
|
|
nUnl.emplace(s);
|
|
}
|
|
validators->setNegativeUNL(nUnl);
|
|
validators->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(validators->quorum() == 39);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
//== with UNL size 60
|
|
//-- with command line minimumQuorum = 50%,
|
|
// seen_reliable affected by nUNL
|
|
auto validators = createValidatorList(60, 30);
|
|
BEAST_EXPECT(validators);
|
|
if (validators)
|
|
{
|
|
hash_set<NodeID> activeValidators;
|
|
hash_set<PublicKey> unl = validators->getTrustedMasterKeys();
|
|
auto it = unl.begin();
|
|
for (std::uint32_t i = 0; i < 50; ++i)
|
|
{
|
|
activeValidators.insert(calcNodeID(*it));
|
|
++it;
|
|
}
|
|
validators->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(validators->quorum() == 48);
|
|
hash_set<PublicKey> nUnl;
|
|
it = unl.begin();
|
|
for (std::uint32_t i = 0; i < 20; ++i)
|
|
{
|
|
nUnl.insert(*it);
|
|
++it;
|
|
}
|
|
validators->setNegativeUNL(nUnl);
|
|
validators->updateTrusted(
|
|
activeValidators,
|
|
env.timeKeeper().now(),
|
|
env.app().getOPs(),
|
|
env.app().overlay(),
|
|
env.app().getHashRouter());
|
|
BEAST_EXPECT(validators->quorum() == 30);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testSha512Hash()
|
|
{
|
|
testcase("Sha512 hashing");
|
|
// Tests that ValidatorList hash_append helpers with a single blob
|
|
// returns the same result as ripple::Sha512Half used by the
|
|
// TMValidatorList protocol message handler
|
|
std::string const manifest = "This is not really a manifest";
|
|
std::string const blob = "This is not really a blob";
|
|
std::string const signature = "This is not really a signature";
|
|
std::uint32_t const version = 1;
|
|
|
|
auto const global = sha512Half(manifest, blob, signature, version);
|
|
BEAST_EXPECT(!!global);
|
|
|
|
std::vector<ValidatorBlobInfo> blobVector(1);
|
|
blobVector[0].blob = blob;
|
|
blobVector[0].signature = signature;
|
|
BEAST_EXPECT(global == sha512Half(manifest, blobVector, version));
|
|
BEAST_EXPECT(global != sha512Half(signature, blobVector, version));
|
|
|
|
{
|
|
std::map<std::size_t, ValidatorBlobInfo> blobMap{
|
|
{99, blobVector[0]}};
|
|
BEAST_EXPECT(global == sha512Half(manifest, blobMap, version));
|
|
BEAST_EXPECT(global != sha512Half(blob, blobMap, version));
|
|
}
|
|
|
|
{
|
|
protocol::TMValidatorList msg1;
|
|
msg1.set_manifest(manifest);
|
|
msg1.set_blob(blob);
|
|
msg1.set_signature(signature);
|
|
msg1.set_version(version);
|
|
BEAST_EXPECT(global == sha512Half(msg1));
|
|
msg1.set_signature(blob);
|
|
BEAST_EXPECT(global != sha512Half(msg1));
|
|
}
|
|
|
|
{
|
|
protocol::TMValidatorListCollection msg2;
|
|
msg2.set_manifest(manifest);
|
|
msg2.set_version(version);
|
|
auto& bi = *msg2.add_blobs();
|
|
bi.set_blob(blob);
|
|
bi.set_signature(signature);
|
|
BEAST_EXPECT(global == sha512Half(msg2));
|
|
bi.set_manifest(manifest);
|
|
BEAST_EXPECT(global != sha512Half(msg2));
|
|
}
|
|
}
|
|
|
|
void
|
|
testBuildMessages()
|
|
{
|
|
testcase("Build and split messages");
|
|
|
|
std::uint32_t const manifestCutoff = 7;
|
|
auto extractHeader = [this](Message& message) {
|
|
auto const& buffer =
|
|
message.getBuffer(compression::Compressed::Off);
|
|
|
|
boost::beast::multi_buffer buffers;
|
|
|
|
// simulate multi-buffer
|
|
auto start = buffer.begin();
|
|
auto end = buffer.end();
|
|
std::vector<std::uint8_t> slice(start, end);
|
|
buffers.commit(boost::asio::buffer_copy(
|
|
buffers.prepare(slice.size()), boost::asio::buffer(slice)));
|
|
|
|
boost::system::error_code ec;
|
|
auto header =
|
|
detail::parseMessageHeader(ec, buffers.data(), buffers.size());
|
|
BEAST_EXPECT(!ec);
|
|
return std::make_pair(header, buffers);
|
|
};
|
|
auto extractProtocolMessage1 = [this,
|
|
&extractHeader](Message& message) {
|
|
auto [header, buffers] = extractHeader(message);
|
|
if (BEAST_EXPECT(header) &&
|
|
BEAST_EXPECT(header->message_type == protocol::mtVALIDATORLIST))
|
|
{
|
|
auto const msg =
|
|
detail::parseMessageContent<protocol::TMValidatorList>(
|
|
*header, buffers.data());
|
|
BEAST_EXPECT(msg);
|
|
return msg;
|
|
}
|
|
return std::shared_ptr<protocol::TMValidatorList>();
|
|
};
|
|
auto extractProtocolMessage2 = [this,
|
|
&extractHeader](Message& message) {
|
|
auto [header, buffers] = extractHeader(message);
|
|
if (BEAST_EXPECT(header) &&
|
|
BEAST_EXPECT(
|
|
header->message_type ==
|
|
protocol::mtVALIDATORLISTCOLLECTION))
|
|
{
|
|
auto const msg = detail::parseMessageContent<
|
|
protocol::TMValidatorListCollection>(
|
|
*header, buffers.data());
|
|
BEAST_EXPECT(msg);
|
|
return msg;
|
|
}
|
|
return std::shared_ptr<protocol::TMValidatorListCollection>();
|
|
};
|
|
auto verifyMessage =
|
|
[this,
|
|
manifestCutoff,
|
|
&extractProtocolMessage1,
|
|
&extractProtocolMessage2](
|
|
auto const version,
|
|
auto const& manifest,
|
|
auto const& blobInfos,
|
|
auto const& messages,
|
|
std::vector<std::pair<std::size_t, std::vector<std::uint32_t>>>
|
|
expectedInfo) {
|
|
BEAST_EXPECT(messages.size() == expectedInfo.size());
|
|
auto msgIter = expectedInfo.begin();
|
|
for (auto const& messageWithHash : messages)
|
|
{
|
|
if (!BEAST_EXPECT(msgIter != expectedInfo.end()))
|
|
break;
|
|
if (!BEAST_EXPECT(messageWithHash.message))
|
|
continue;
|
|
auto const& expectedSeqs = msgIter->second;
|
|
auto seqIter = expectedSeqs.begin();
|
|
auto const size =
|
|
messageWithHash.message
|
|
->getBuffer(compression::Compressed::Off)
|
|
.size();
|
|
// This size is arbitrary, but shouldn't change
|
|
BEAST_EXPECT(size == msgIter->first);
|
|
if (expectedSeqs.size() == 1)
|
|
{
|
|
auto const msg =
|
|
extractProtocolMessage1(*messageWithHash.message);
|
|
auto const expectedVersion = 1;
|
|
if (BEAST_EXPECT(msg))
|
|
{
|
|
BEAST_EXPECT(msg->version() == expectedVersion);
|
|
if (!BEAST_EXPECT(seqIter != expectedSeqs.end()))
|
|
continue;
|
|
auto const& expectedBlob = blobInfos.at(*seqIter);
|
|
BEAST_EXPECT(
|
|
(*seqIter < manifestCutoff) ==
|
|
!!expectedBlob.manifest);
|
|
auto const expectedManifest =
|
|
*seqIter < manifestCutoff &&
|
|
expectedBlob.manifest
|
|
? *expectedBlob.manifest
|
|
: manifest;
|
|
BEAST_EXPECT(msg->manifest() == expectedManifest);
|
|
BEAST_EXPECT(msg->blob() == expectedBlob.blob);
|
|
BEAST_EXPECT(
|
|
msg->signature() == expectedBlob.signature);
|
|
++seqIter;
|
|
BEAST_EXPECT(seqIter == expectedSeqs.end());
|
|
|
|
BEAST_EXPECT(
|
|
messageWithHash.hash ==
|
|
sha512Half(
|
|
expectedManifest,
|
|
expectedBlob.blob,
|
|
expectedBlob.signature,
|
|
expectedVersion));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::vector<ValidatorBlobInfo> hashingBlobs;
|
|
hashingBlobs.reserve(msgIter->second.size());
|
|
|
|
auto const msg =
|
|
extractProtocolMessage2(*messageWithHash.message);
|
|
if (BEAST_EXPECT(msg))
|
|
{
|
|
BEAST_EXPECT(msg->version() == version);
|
|
BEAST_EXPECT(msg->manifest() == manifest);
|
|
for (auto const& blobInfo : msg->blobs())
|
|
{
|
|
if (!BEAST_EXPECT(
|
|
seqIter != expectedSeqs.end()))
|
|
break;
|
|
auto const& expectedBlob =
|
|
blobInfos.at(*seqIter);
|
|
hashingBlobs.push_back(expectedBlob);
|
|
BEAST_EXPECT(
|
|
blobInfo.has_manifest() ==
|
|
!!expectedBlob.manifest);
|
|
BEAST_EXPECT(
|
|
blobInfo.has_manifest() ==
|
|
(*seqIter < manifestCutoff));
|
|
|
|
if (*seqIter < manifestCutoff)
|
|
BEAST_EXPECT(
|
|
blobInfo.manifest() ==
|
|
*expectedBlob.manifest);
|
|
BEAST_EXPECT(
|
|
blobInfo.blob() == expectedBlob.blob);
|
|
BEAST_EXPECT(
|
|
blobInfo.signature() ==
|
|
expectedBlob.signature);
|
|
++seqIter;
|
|
}
|
|
BEAST_EXPECT(seqIter == expectedSeqs.end());
|
|
}
|
|
BEAST_EXPECT(
|
|
messageWithHash.hash ==
|
|
sha512Half(manifest, hashingBlobs, version));
|
|
}
|
|
++msgIter;
|
|
}
|
|
BEAST_EXPECT(msgIter == expectedInfo.end());
|
|
};
|
|
auto verifyBuildMessages =
|
|
[this](
|
|
std::pair<std::size_t, std::size_t> const& result,
|
|
std::size_t expectedSequence,
|
|
std::size_t expectedSize) {
|
|
BEAST_EXPECT(result.first == expectedSequence);
|
|
BEAST_EXPECT(result.second == expectedSize);
|
|
};
|
|
|
|
std::string const manifest = "This is not a manifest";
|
|
std::uint32_t const version = 2;
|
|
// Mutable so items can be removed in later tests.
|
|
auto const blobInfos = [manifestCutoff = manifestCutoff]() {
|
|
std::map<std::size_t, ValidatorBlobInfo> bis;
|
|
|
|
for (auto seq : {5, 6, 7, 10, 12})
|
|
{
|
|
auto& b = bis[seq];
|
|
std::stringstream s;
|
|
s << "This is not a blob with sequence " << seq;
|
|
b.blob = s.str();
|
|
s.str(std::string());
|
|
s << "This is not a signature for sequence " << seq;
|
|
b.signature = s.str();
|
|
if (seq < manifestCutoff)
|
|
{
|
|
// add a manifest for the "early" blobs
|
|
s.str(std::string());
|
|
s << "This is not manifest " << seq;
|
|
b.manifest = s.str();
|
|
}
|
|
}
|
|
return bis;
|
|
}();
|
|
auto const maxSequence = blobInfos.rbegin()->first;
|
|
BEAST_EXPECT(maxSequence == 12);
|
|
|
|
std::vector<ValidatorList::MessageWithHash> messages;
|
|
|
|
// Version 1
|
|
|
|
// This peer has a VL ahead of our "current"
|
|
verifyBuildMessages(
|
|
ValidatorList::buildValidatorListMessages(
|
|
1, 8, maxSequence, version, manifest, blobInfos, messages),
|
|
0,
|
|
0);
|
|
BEAST_EXPECT(messages.size() == 0);
|
|
|
|
// Don't repeat the work if messages is populated, even though the
|
|
// peerSequence provided indicates it should. Note that this
|
|
// situation is contrived for this test and should never happen in
|
|
// real code.
|
|
messages.emplace_back();
|
|
verifyBuildMessages(
|
|
ValidatorList::buildValidatorListMessages(
|
|
1, 3, maxSequence, version, manifest, blobInfos, messages),
|
|
5,
|
|
0);
|
|
BEAST_EXPECT(messages.size() == 1 && !messages.front().message);
|
|
|
|
// Generate a version 1 message
|
|
messages.clear();
|
|
verifyBuildMessages(
|
|
ValidatorList::buildValidatorListMessages(
|
|
1, 3, maxSequence, version, manifest, blobInfos, messages),
|
|
5,
|
|
1);
|
|
if (BEAST_EXPECT(messages.size() == 1) &&
|
|
BEAST_EXPECT(messages.front().message))
|
|
{
|
|
auto const& messageWithHash = messages.front();
|
|
auto const msg = extractProtocolMessage1(*messageWithHash.message);
|
|
auto const size =
|
|
messageWithHash.message->getBuffer(compression::Compressed::Off)
|
|
.size();
|
|
// This size is arbitrary, but shouldn't change
|
|
BEAST_EXPECT(size == 108);
|
|
auto const& expected = blobInfos.at(5);
|
|
if (BEAST_EXPECT(msg))
|
|
{
|
|
BEAST_EXPECT(msg->version() == 1);
|
|
BEAST_EXPECT(msg->manifest() == *expected.manifest);
|
|
BEAST_EXPECT(msg->blob() == expected.blob);
|
|
BEAST_EXPECT(msg->signature() == expected.signature);
|
|
}
|
|
BEAST_EXPECT(
|
|
messageWithHash.hash ==
|
|
sha512Half(
|
|
*expected.manifest, expected.blob, expected.signature, 1));
|
|
}
|
|
|
|
// Version 2
|
|
|
|
messages.clear();
|
|
|
|
// This peer has a VL ahead of us.
|
|
verifyBuildMessages(
|
|
ValidatorList::buildValidatorListMessages(
|
|
2,
|
|
maxSequence * 2,
|
|
maxSequence,
|
|
version,
|
|
manifest,
|
|
blobInfos,
|
|
messages),
|
|
0,
|
|
0);
|
|
BEAST_EXPECT(messages.size() == 0);
|
|
|
|
// Don't repeat the work if messages is populated, even though the
|
|
// peerSequence provided indicates it should. Note that this
|
|
// situation is contrived for this test and should never happen in
|
|
// real code.
|
|
messages.emplace_back();
|
|
verifyBuildMessages(
|
|
ValidatorList::buildValidatorListMessages(
|
|
2, 3, maxSequence, version, manifest, blobInfos, messages),
|
|
maxSequence,
|
|
0);
|
|
BEAST_EXPECT(messages.size() == 1 && !messages.front().message);
|
|
|
|
// Generate a version 2 message. Don't send the current
|
|
messages.clear();
|
|
verifyBuildMessages(
|
|
ValidatorList::buildValidatorListMessages(
|
|
2, 5, maxSequence, version, manifest, blobInfos, messages),
|
|
maxSequence,
|
|
4);
|
|
verifyMessage(
|
|
version, manifest, blobInfos, messages, {{372, {6, 7, 10, 12}}});
|
|
|
|
// Test message splitting on size limits.
|
|
|
|
// Set a limit that should give two messages
|
|
messages.clear();
|
|
verifyBuildMessages(
|
|
ValidatorList::buildValidatorListMessages(
|
|
2, 5, maxSequence, version, manifest, blobInfos, messages, 300),
|
|
maxSequence,
|
|
4);
|
|
verifyMessage(
|
|
version,
|
|
manifest,
|
|
blobInfos,
|
|
messages,
|
|
{{212, {6, 7}}, {192, {10, 12}}});
|
|
|
|
// Set a limit between the size of the two earlier messages so one
|
|
// will split and the other won't
|
|
messages.clear();
|
|
verifyBuildMessages(
|
|
ValidatorList::buildValidatorListMessages(
|
|
2, 5, maxSequence, version, manifest, blobInfos, messages, 200),
|
|
maxSequence,
|
|
4);
|
|
verifyMessage(
|
|
version,
|
|
manifest,
|
|
blobInfos,
|
|
messages,
|
|
{{108, {6}}, {108, {7}}, {192, {10, 12}}});
|
|
|
|
// Set a limit so that all the VLs are sent individually
|
|
messages.clear();
|
|
verifyBuildMessages(
|
|
ValidatorList::buildValidatorListMessages(
|
|
2, 5, maxSequence, version, manifest, blobInfos, messages, 150),
|
|
maxSequence,
|
|
4);
|
|
verifyMessage(
|
|
version,
|
|
manifest,
|
|
blobInfos,
|
|
messages,
|
|
{{108, {6}}, {108, {7}}, {110, {10}}, {110, {12}}});
|
|
|
|
// Set a limit smaller than some of the messages. Because single
|
|
// messages send regardless, they will all still be sent
|
|
messages.clear();
|
|
verifyBuildMessages(
|
|
ValidatorList::buildValidatorListMessages(
|
|
2, 5, maxSequence, version, manifest, blobInfos, messages, 108),
|
|
maxSequence,
|
|
4);
|
|
verifyMessage(
|
|
version,
|
|
manifest,
|
|
blobInfos,
|
|
messages,
|
|
{{108, {6}}, {108, {7}}, {110, {10}}, {110, {12}}});
|
|
}
|
|
|
|
public:
|
|
void
|
|
run() override
|
|
{
|
|
testGenesisQuorum();
|
|
testConfigLoad();
|
|
testApplyLists();
|
|
testGetAvailable();
|
|
testUpdateTrusted();
|
|
testExpires();
|
|
testNegativeUNL();
|
|
testSha512Hash();
|
|
testBuildMessages();
|
|
}
|
|
}; // namespace test
|
|
|
|
BEAST_DEFINE_TESTSUITE(ValidatorList, app, ripple);
|
|
|
|
} // namespace test
|
|
} // namespace ripple
|