//------------------------------------------------------------------------------ /* 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 #include #include #include #include #include #include #include #include #include #include #include #include #include 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::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(s.data()), s.size()); } static std::string makeRevocationString(PublicKey const& pk, SecretKey const& sk) { STObject st(sfGeneric); st[sfSequence] = std::numeric_limits::max(); st[sfPublicKey] = pk; sign( st, HashPrefix::manifest, *publicKeyType(pk), sk, sfMasterSignature); Serializer s; st.add(s); return std::string(static_cast(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 const& validators, std::size_t sequence, std::size_t validUntil, std::optional 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 const& keys) { auto const data = base64_decode(blob); return strHex(sign(keys.first, keys.second, makeSlice(data))); } static hash_set asNodeIDs(std::initializer_list const& pks) { hash_set 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( 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( 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 const emptyCfgKeys; std::vector 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 configList; configList.reserve(8); while (configList.size() != 8) configList.push_back(randomNode()); // Correct configuration std::vector 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( 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( 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 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( manifests, manifests, env.timeKeeper(), app.config().legacy("database_path"), env.journal); auto const localSigningPublic = parseBase58(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( 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( 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( manifests, manifests, env.timeKeeper(), app.config().legacy("database_path"), env.journal); // load should reject invalid validator list signing keys std::vector badPublishers({"NotASigningKey"}); BEAST_EXPECT( !trustedKeys->load(emptyLocalKey, emptyCfgKeys, badPublishers)); // load should reject validator list signing keys with invalid // encoding std::vector 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 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( 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::max()))); // this one is not revoked (and not in manifest cache at all.) auto legitKey = randomMasterKey(); std::vector 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> 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( manifests, manifests, env.app().timeKeeper(), app.config().legacy("database_path"), env.journal); auto expectTrusted = [this, &trustedKeys](std::vector 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 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 cfgKeys1({strHex(publisherPublic)}); PublicKey emptyLocalKey; std::vector emptyCfgKeys; BEAST_EXPECT(trustedKeys->load(emptyLocalKey, emptyCfgKeys, cfgKeys1)); std::map> const lists = []() { auto constexpr listSize = 20; auto constexpr numLists = 9; std::map> 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( 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 cfgKeys1({strHex(publisherPublic)}); PublicKey emptyLocalKey; std::vector emptyCfgKeys; BEAST_EXPECT(trustedKeys->load(emptyLocalKey, emptyCfgKeys, cfgKeys1)); std::vector const list = []() { auto constexpr listSize = 20; std::vector 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( manifestsOuter, manifestsOuter, env.timeKeeper(), app.config().legacy("database_path"), env.journal); std::vector cfgPublishersOuter; hash_set activeValidatorsOuter; std::size_t const maxKeys = 40; { std::vector cfgKeys; cfgKeys.reserve(maxKeys); hash_set 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(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 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( manifestsOuter, manifestsOuter, env.timeKeeper(), app.config().legacy("database_path"), env.journal); auto const publisherSecret = randomSecretKey(); auto const publisherPublic = derivePublicKey(KeyType::ed25519, publisherSecret); std::vector cfgPublishers({strHex(publisherPublic)}); std::vector 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::max()); } { // Should use custom minimum quorum std::size_t const minQuorum = 1; ManifestCache manifests; auto trustedKeys = std::make_unique( manifests, manifests, env.timeKeeper(), app.config().legacy("database_path"), env.journal, minQuorum); std::size_t n = 10; std::vector cfgKeys; cfgKeys.reserve(n); hash_set expectedTrusted; hash_set 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( manifestsOuter, manifestsOuter, env.app().timeKeeper(), app.config().legacy("database_path"), env.journal); PublicKey emptyLocalKey; std::vector 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 cfgKeys({strHex(publisherKeys.first)}); BEAST_EXPECT( trustedKeys->load(emptyLocalKey, emptyCfgKeys, cfgKeys)); std::vector list({randomValidator(), randomValidator()}); hash_set 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::max()); // (Re)trust validators from new valid list std::vector 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( manifestsOuter, manifestsOuter, env.timeKeeper(), app.config().legacy("database_path"), env.journal); std::vector cfgPublishers; hash_set activeValidators; hash_set activeKeys; std::vector 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( manifestsOuter, manifestsOuter, env.timeKeeper(), app.config().legacy("database_path"), env.journal); auto const localKey = randomNode(); std::vector cfgPublishers; hash_set activeValidators; hash_set activeKeys; std::vector 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( manifests, manifests, env.timeKeeper(), app.config().legacy("database_path"), env.journal); hash_set activeValidators; std::vector 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 cfgPublishers( {strHex(publisherPublic)}); PublicKey emptyLocalKey; std::vector 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 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( 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( manifests, manifests, env.app().timeKeeper(), app.config().legacy("database_path"), env.journal); std::vector validators = {randomValidator()}; hash_set 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 blobs; int version; std::vector 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 cfgPublishers( {strHex(publisherPublic)}); PublicKey emptyLocalKey; std::vector 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 minimumQuorum = {}) -> std::shared_ptr { auto trustedKeys = std::make_shared( manifests, manifests, env.timeKeeper(), env.app().config().legacy("database_path"), env.journal, minimumQuorum); std::vector cfgPublishers; std::vector cfgKeys; hash_set 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 activeValidators; //== Combinations == std::array unlSizes = {34, 35, 39, 60}; std::array 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 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::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 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 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 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 activeValidators; hash_set 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 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 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 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 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( *header, buffers.data()); BEAST_EXPECT(msg); return msg; } return std::shared_ptr(); }; 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(); }; auto verifyMessage = [this, manifestCutoff, &extractProtocolMessage1, &extractProtocolMessage2]( auto const version, auto const& manifest, auto const& blobInfos, auto const& messages, std::vector>> 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 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 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 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 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