diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index b0f3a902664..8829f8e3a76 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -87,9 +87,10 @@ The `network_id` field was added in the `server_info` response in version 1.5.0 As of 2025-01-23, version 2.4.0 is in development. You can use a pre-release version by building from source or [using the `nightly` package](https://xrpl.org/docs/infrastructure/installation/install-rippled-on-ubuntu). -### Addition in 2.4 +### Additions and bugfixes in 2.4.0 - `ledger_entry`: `state` is added an alias for `ripple_state`. +- `validators`: Added new field `validator_list_threshold` in response. ## XRP Ledger server version 2.3.0 diff --git a/cfg/validators-example.txt b/cfg/validators-example.txt index 8f7c04729e0..5b59e5c4fde 100644 --- a/cfg/validators-example.txt +++ b/cfg/validators-example.txt @@ -70,3 +70,21 @@ ED45D1840EE724BE327ABE9146503D5848EFD5F38B6D5FEDE71E80ACCE5E6E738B # # [validator_list_keys] # ED264807102805220DA0F312E71FC2C69E1552C9C5790F6C25E3729DEB573D5860 + + +# [validator_list_threshold] +# +# Minimum number of validator lists on which a validator must be listed in +# order to be used. +# +# This can be set explicitly to any positive integer number not greater than +# the size of [validator_list_keys]. If it is not set, or set to 0, the +# value will be calculated at startup from the size of [validator_list_keys], +# where the calculation is: +# +# threshold = size(validator_list_keys) < 3 +# ? 1 +# : floor(size(validator_list_keys) / 2) + 1 + +[validator_list_threshold] +0 diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index c41d7ef2594..81f81b7c6b9 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -664,27 +664,28 @@ JSS(validated); // out: NetworkOPs, RPCHelpers, AccountTx* JSS(validator_list_expires); // out: NetworkOps, ValidatorList JSS(validator_list); // out: NetworkOps, ValidatorList JSS(validators); -JSS(validated_hash); // out: NetworkOPs -JSS(validated_ledger); // out: NetworkOPs -JSS(validated_ledger_index); // out: SubmitTransaction -JSS(validated_ledgers); // out: NetworkOPs -JSS(validation_key); // out: ValidationCreate, ValidationSeed -JSS(validation_private_key); // out: ValidationCreate -JSS(validation_public_key); // out: ValidationCreate, ValidationSeed -JSS(validation_quorum); // out: NetworkOPs -JSS(validation_seed); // out: ValidationCreate, ValidationSeed -JSS(validations); // out: AmendmentTableImpl -JSS(validator_sites); // out: ValidatorSites -JSS(value); // out: STAmount -JSS(version); // out: RPCVersion -JSS(vetoed); // out: AmendmentTableImpl -JSS(volume_a); // out: BookChanges -JSS(volume_b); // out: BookChanges -JSS(vote); // in: Feature -JSS(vote_slots); // out: amm_info -JSS(vote_weight); // out: amm_info -JSS(warning); // rpc: -JSS(warnings); // out: server_info, server_state +JSS(validated_hash); // out: NetworkOPs +JSS(validated_ledger); // out: NetworkOPs +JSS(validated_ledger_index); // out: SubmitTransaction +JSS(validated_ledgers); // out: NetworkOPs +JSS(validation_key); // out: ValidationCreate, ValidationSeed +JSS(validation_private_key); // out: ValidationCreate +JSS(validation_public_key); // out: ValidationCreate, ValidationSeed +JSS(validation_quorum); // out: NetworkOPs +JSS(validation_seed); // out: ValidationCreate, ValidationSeed +JSS(validations); // out: AmendmentTableImpl +JSS(validator_list_threshold); // out: ValidatorList +JSS(validator_sites); // out: ValidatorSites +JSS(value); // out: STAmount +JSS(version); // out: RPCVersion +JSS(vetoed); // out: AmendmentTableImpl +JSS(volume_a); // out: BookChanges +JSS(volume_b); // out: BookChanges +JSS(vote); // in: Feature +JSS(vote_slots); // out: amm_info +JSS(vote_weight); // out: amm_info +JSS(warning); // rpc: +JSS(warnings); // out: server_info, server_state JSS(workers); JSS(write_load); // out: GetCounts // clang-format on diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index 05989c0f601..465723b1a70 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -426,6 +426,33 @@ class ValidatorList_test : public beast::unit_test::suite BEAST_EXPECT(trustedKeys->load({}, emptyCfgKeys, cfgPublishers)); for (auto const& key : keys) BEAST_EXPECT(trustedKeys->trustedPublisher(key)); + BEAST_EXPECT( + trustedKeys->getListThreshold() == keys.size() / 2 + 1); + } + { + ManifestCache manifests; + auto trustedKeys = std::make_unique( + manifests, + manifests, + env.timeKeeper(), + app.config().legacy("database_path"), + env.journal); + + std::vector keys( + {randomMasterKey(), + randomMasterKey(), + randomMasterKey(), + randomMasterKey()}); + std::vector cfgPublishers; + for (auto const& key : keys) + cfgPublishers.push_back(strHex(key)); + + // explicitly set the list threshold + BEAST_EXPECT(trustedKeys->load( + {}, emptyCfgKeys, cfgPublishers, std::size_t(2))); + for (auto const& key : keys) + BEAST_EXPECT(trustedKeys->trustedPublisher(key)); + BEAST_EXPECT(trustedKeys->getListThreshold() == 2); } { // Attempt to load a publisher key that has been revoked. @@ -452,15 +479,57 @@ class ValidatorList_test : public beast::unit_test::suite pubRevokedSigning.second, std::numeric_limits::max()))); - // this one is not revoked (and not in manifest cache at all.) + // these two are not revoked (and not in the manifest cache at all.) + auto legitKey1 = randomMasterKey(); + auto legitKey2 = randomMasterKey(); + + std::vector cfgPublishers = { + strHex(pubRevokedPublic), strHex(legitKey1), strHex(legitKey2)}; + BEAST_EXPECT(trustedKeys->load({}, emptyCfgKeys, cfgPublishers)); + + BEAST_EXPECT(!trustedKeys->trustedPublisher(pubRevokedPublic)); + BEAST_EXPECT(trustedKeys->trustedPublisher(legitKey1)); + BEAST_EXPECT(trustedKeys->trustedPublisher(legitKey2)); + // 2 is the threshold for 3 publishers (even though 1 is revoked) + BEAST_EXPECT(trustedKeys->getListThreshold() == 2); + } + { + // One (of two) publisher keys has been revoked, the user had + // explicitly set validator list threshold to 2. + 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 the manifest cache at all.) auto legitKey = randomMasterKey(); std::vector cfgPublishers = { strHex(pubRevokedPublic), strHex(legitKey)}; - BEAST_EXPECT(trustedKeys->load({}, emptyCfgKeys, cfgPublishers)); + BEAST_EXPECT(trustedKeys->load( + {}, emptyCfgKeys, cfgPublishers, std::size_t(2))); BEAST_EXPECT(!trustedKeys->trustedPublisher(pubRevokedPublic)); BEAST_EXPECT(trustedKeys->trustedPublisher(legitKey)); + // 2 is the threshold, as requested in configuration + BEAST_EXPECT(trustedKeys->getListThreshold() == 2); } } @@ -1263,6 +1332,44 @@ class ValidatorList_test : public beast::unit_test::suite trustedKeys->quorum() == std::numeric_limits::max()); } + { + // Trust explicitly listed validators also when list threshold is + // higher than 1 + auto trustedKeys = std::make_unique( + manifestsOuter, + manifestsOuter, + env.timeKeeper(), + app.config().legacy("database_path"), + env.journal); + auto const masterPrivate = randomSecretKey(); + auto const masterPublic = + derivePublicKey(KeyType::ed25519, masterPrivate); + std::vector cfgKeys( + {toBase58(TokenType::NodePublic, masterPublic)}); + + auto const publisher1Secret = randomSecretKey(); + auto const publisher1Public = + derivePublicKey(KeyType::ed25519, publisher1Secret); + auto const publisher2Secret = randomSecretKey(); + auto const publisher2Public = + derivePublicKey(KeyType::ed25519, publisher2Secret); + std::vector cfgPublishers( + {strHex(publisher1Public), strHex(publisher2Public)}); + + BEAST_EXPECT( + trustedKeys->load({}, cfgKeys, cfgPublishers, std::size_t(2))); + + 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.size() == 1); + BEAST_EXPECT(trustedKeys->listed(masterPublic)); + BEAST_EXPECT(trustedKeys->trusted(masterPublic)); + } { // Should use custom minimum quorum std::size_t const minQuorum = 1; @@ -1530,11 +1637,22 @@ class ValidatorList_test : public beast::unit_test::suite calcNodeID(valKeys.back().masterPublic)); } - auto addPublishedList = [this, - &env, - &trustedKeys, - &valKeys, - &siteUri]() { + // locals[0]: from 0 to maxKeys - 4 + // locals[1]: from 1 to maxKeys - 2 + // locals[2]: from 2 to maxKeys + constexpr static int publishers = 3; + std::array< + std::pair< + decltype(valKeys)::const_iterator, + decltype(valKeys)::const_iterator>, + publishers> + locals = { + std::make_pair(valKeys.cbegin(), valKeys.cend() - 4), + std::make_pair(valKeys.cbegin() + 1, valKeys.cend() - 2), + std::make_pair(valKeys.cbegin() + 2, valKeys.cend()), + }; + + auto addPublishedList = [&, this](int i) { auto const publisherSecret = randomSecretKey(); auto const publisherPublic = derivePublicKey(KeyType::ed25519, publisherSecret); @@ -1550,16 +1668,19 @@ class ValidatorList_test : public beast::unit_test::suite {strHex(publisherPublic)}); std::vector emptyCfgKeys; - BEAST_EXPECT( - trustedKeys->load({}, emptyCfgKeys, cfgPublishers)); + // Threshold of 1 will result in a union of all the lists + BEAST_EXPECT(trustedKeys->load( + {}, emptyCfgKeys, cfgPublishers, std::size_t(1))); auto const version = 1; auto const sequence = 1; using namespace std::chrono_literals; NetClock::time_point const validUntil = env.timeKeeper().now() + 3600s; + std::vector localKeys{ + locals[i].first, locals[i].second}; auto const blob = makeList( - valKeys, sequence, validUntil.time_since_epoch().count()); + localKeys, sequence, validUntil.time_since_epoch().count()); auto const sig = signList(blob, pubSigningKeys); BEAST_EXPECT( @@ -1571,8 +1692,9 @@ class ValidatorList_test : public beast::unit_test::suite }; // Apply multiple published lists - for (auto i = 0; i < 3; ++i) - addPublishedList(); + for (auto i = 0; i < publishers; ++i) + addPublishedList(i); + BEAST_EXPECT(trustedKeys->getListThreshold() == 1); TrustChanges changes = trustedKeys->updateTrusted( activeValidators, @@ -1593,6 +1715,191 @@ class ValidatorList_test : public beast::unit_test::suite BEAST_EXPECT(changes.added == added); BEAST_EXPECT(changes.removed.empty()); } + { + // Trusted set should include validators from intersection of 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)); + } + + // locals[0]: from 0 to maxKeys - 4 + // locals[1]: from 1 to maxKeys - 2 + // locals[2]: from 2 to maxKeys + // interesection of at least 2: same as locals[1] + // intersection when 1 is dropped: from 2 to maxKeys - 4 + constexpr static int publishers = 3; + std::array< + std::pair< + decltype(valKeys)::const_iterator, + decltype(valKeys)::const_iterator>, + publishers> + locals = { + std::make_pair(valKeys.cbegin(), valKeys.cend() - 4), + std::make_pair(valKeys.cbegin() + 1, valKeys.cend() - 2), + std::make_pair(valKeys.cbegin() + 2, valKeys.cend()), + }; + + auto addPublishedList = [&, this]( + int i, + NetClock::time_point& validUntil1, + NetClock::time_point& validUntil2) { + 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)}); + std::vector emptyCfgKeys; + + BEAST_EXPECT( + trustedKeys->load({}, emptyCfgKeys, cfgPublishers)); + + auto const version = 1; + auto const sequence = 1; + using namespace std::chrono_literals; + // Want to drop 1 sooner + NetClock::time_point const validUntil = env.timeKeeper().now() + + (i == 2 ? 120s + : i == 1 ? 60s + : 3600s); + if (i == 1) + validUntil1 = validUntil; + else if (i == 2) + validUntil2 = validUntil; + std::vector localKeys{ + locals[i].first, locals[i].second}; + auto const blob = makeList( + localKeys, 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 + // validUntil1 is expiration time for locals[1] + NetClock::time_point validUntil1, validUntil2; + for (auto i = 0; i < publishers; ++i) + addPublishedList(i, validUntil1, validUntil2); + BEAST_EXPECT(trustedKeys->getListThreshold() == 2); + + 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() - 3) * 0.8f)); + + for (auto const& val : valKeys) + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + + hash_set added; + for (std::size_t i = 0; i < maxKeys; ++i) + { + auto const& val = valKeys[i]; + if (i >= 1 && i < maxKeys - 2) + { + BEAST_EXPECT(trustedKeys->trusted(val.masterPublic)); + added.insert(calcNodeID(val.masterPublic)); + } + else + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + } + BEAST_EXPECT(changes.added == added); + BEAST_EXPECT(changes.removed.empty()); + + // Expire locals[1] + env.timeKeeper().set(validUntil1); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + + BEAST_EXPECT( + trustedKeys->quorum() == + std::ceil((valKeys.size() - 6) * 0.8f)); + + for (auto const& val : valKeys) + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + + hash_set removed; + for (std::size_t i = 0; i < maxKeys; ++i) + { + auto const& val = valKeys[i]; + if (i >= 2 && i < maxKeys - 4) + BEAST_EXPECT(trustedKeys->trusted(val.masterPublic)); + else + { + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + if (i >= 1 && i < maxKeys - 2) + removed.insert(calcNodeID(val.masterPublic)); + } + } + + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + + // Expire locals[2], which removes all validators + env.timeKeeper().set(validUntil2); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + + BEAST_EXPECT( + trustedKeys->quorum() == + std::numeric_limits::max()); + + removed.clear(); + for (std::size_t i = 0; i < maxKeys; ++i) + { + auto const& val = valKeys[i]; + if (i < maxKeys - 4) + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + else + BEAST_EXPECT(!trustedKeys->listed(val.masterPublic)); + + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + if (i >= 2 && i < maxKeys - 4) + removed.insert(calcNodeID(val.masterPublic)); + } + + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } } void @@ -2390,6 +2697,1436 @@ class ValidatorList_test : public beast::unit_test::suite {{108, {6}}, {108, {7}}, {110, {10}}, {110, {12}}}); } + void + testQuorumDisabled() + { + testcase("Test quorum disabled"); + + std::string const siteUri = "testQuorumDisabled.test"; + jtx::Env env(*this); + auto& app = env.app(); + + constexpr std::size_t maxKeys = 20; + hash_set activeValidators; + std::vector valKeys; + while (valKeys.size() != maxKeys) + { + valKeys.push_back(randomValidator()); + activeValidators.emplace(calcNodeID(valKeys.back().masterPublic)); + } + + struct Publisher + { + bool revoked; + PublicKey pubKey; + std::pair signingKeys; + std::string manifest; + NetClock::time_point expiry = {}; + }; + + // Create ValidatorList with a set of countTotal publishers, of which + // first countRevoked are revoked and the last one expires early + auto makeValidatorList = [&, this]( + std::size_t countTotal, + std::size_t countRevoked, + std::size_t listThreshold, + ManifestCache& pubManifests, + ManifestCache& valManifests, + std::optional self, + std::vector& publishers // out + ) -> std::unique_ptr { + auto result = std::make_unique( + valManifests, + pubManifests, + env.timeKeeper(), + app.config().legacy("database_path"), + env.journal); + + std::vector cfgPublishers; + for (std::size_t i = 0; i < countTotal; ++i) + { + auto const publisherSecret = randomSecretKey(); + auto const publisherPublic = + derivePublicKey(KeyType::ed25519, publisherSecret); + auto const pubSigningKeys = randomKeyPair(KeyType::secp256k1); + cfgPublishers.push_back(strHex(publisherPublic)); + + constexpr auto revoked = + std::numeric_limits::max(); + auto const manifest = base64_encode(makeManifestString( + publisherPublic, + publisherSecret, + pubSigningKeys.first, + pubSigningKeys.second, + i < countRevoked ? revoked : 1)); + publishers.push_back(Publisher{ + i < countRevoked, + publisherPublic, + pubSigningKeys, + manifest}); + } + + std::vector const emptyCfgKeys; + auto threshold = + listThreshold > 0 ? std::optional(listThreshold) : std::nullopt; + if (self) + { + valManifests.applyManifest( + *deserializeManifest(base64_decode(self->manifest))); + BEAST_EXPECT(result->load( + self->signingPublic, + emptyCfgKeys, + cfgPublishers, + threshold)); + } + else + { + BEAST_EXPECT( + result->load({}, emptyCfgKeys, cfgPublishers, threshold)); + } + + for (std::size_t i = 0; i < countTotal; ++i) + { + using namespace std::chrono_literals; + publishers[i].expiry = env.timeKeeper().now() + + (i == countTotal - 1 ? 60s : 3600s); + auto const blob = makeList( + valKeys, + 1, + publishers[i].expiry.time_since_epoch().count()); + auto const sig = signList(blob, publishers[i].signingKeys); + + BEAST_EXPECT( + result + ->applyLists( + publishers[i].manifest, + 1, + {{blob, sig, {}}}, + siteUri) + .bestDisposition() == + (publishers[i].revoked ? ListDisposition::untrusted + : ListDisposition::accepted)); + } + + return result; + }; + + // Test cases use 5 publishers. + constexpr auto quorumDisabled = std::numeric_limits::max(); + { + // List threshold = 5 (same as number of trusted publishers) + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is a random validator + auto const self = randomValidator(); + auto const keysTotal = valKeys.size() + 1; + auto trustedKeys = makeValidatorList( + 5, // + 0, + 5, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 5); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + hash_set added; + added.insert(calcNodeID(self.masterPublic)); + 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()); + + // Expire one publisher - only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 5 (same as number of trusted publishers) + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 5, // + 0, + 5, + pubManifests, + valManifests, + {}, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 5); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + + // Expire one publisher - no trusted validators + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 0); + + hash_set removed; + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 4, 1 publisher is revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is in UNL + auto const self = valKeys[1]; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 5, // + 1, + 4, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 4); + int untrustedCount = 0; + for (auto const& p : publishers) + { + bool const trusted = trustedKeys->trustedPublisher(p.pubKey); + BEAST_EXPECT(p.revoked ^ trusted); + untrustedCount += trusted ? 0 : 1; + } + BEAST_EXPECT(untrustedCount == 1); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + + // Expire one publisher - only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + if (val.masterPublic != self.masterPublic) + { + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 3 (default), 2 publishers are revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is a random validator + auto const self = randomValidator(); + auto const keysTotal = valKeys.size() + 1; + auto trustedKeys = makeValidatorList( + 5, // + 2, + 0, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 3); + int untrustedCount = 0; + for (auto const& p : publishers) + { + bool const trusted = trustedKeys->trustedPublisher(p.pubKey); + BEAST_EXPECT(p.revoked ^ trusted); + untrustedCount += trusted ? 0 : 1; + } + BEAST_EXPECT(untrustedCount == 2); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + hash_set added; + added.insert(calcNodeID(self.masterPublic)); + 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()); + + // Expire one publisher - no quorum, only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 3 (default), 2 publishers are revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is in UNL + auto const self = valKeys[5]; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 5, // + 2, + 0, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 3); + int untrustedCount = 0; + for (auto const& p : publishers) + { + bool const trusted = trustedKeys->trustedPublisher(p.pubKey); + BEAST_EXPECT(p.revoked ^ trusted); + untrustedCount += trusted ? 0 : 1; + } + BEAST_EXPECT(untrustedCount == 2); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + + // Expire one publisher - no quorum, only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + if (val.masterPublic != self.masterPublic) + { + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 3 (default), 2 publishers are revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 5, // + 2, + 0, + pubManifests, + valManifests, + {}, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 3); + int untrustedCount = 0; + for (auto const& p : publishers) + { + bool const trusted = trustedKeys->trustedPublisher(p.pubKey); + BEAST_EXPECT(p.revoked ^ trusted); + untrustedCount += trusted ? 0 : 1; + } + BEAST_EXPECT(untrustedCount == 2); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + + // Expire one publisher - no quorum, no trusted validators + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 0); + + hash_set removed; + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 2, 1 publisher is revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is a random validator + auto const self = randomValidator(); + auto const keysTotal = valKeys.size() + 1; + auto trustedKeys = makeValidatorList( + 5, // + 1, + 2, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 2); + int untrustedCount = 0; + for (auto const& p : publishers) + { + bool const trusted = trustedKeys->trustedPublisher(p.pubKey); + BEAST_EXPECT(p.revoked ^ trusted); + untrustedCount += trusted ? 0 : 1; + } + BEAST_EXPECT(untrustedCount == 1); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + hash_set added; + added.insert(calcNodeID(self.masterPublic)); + 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()); + + // Expire one publisher - no quorum + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(trustedKeys->trusted(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed.empty()); + } + { + // List threshold = 1 + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is a random validator + auto const self = randomValidator(); + auto const keysTotal = valKeys.size() + 1; + auto trustedKeys = makeValidatorList( + 5, // + 0, + 1, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 1); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + hash_set added; + added.insert(calcNodeID(self.masterPublic)); + 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()); + + // Expire one publisher - no quorum + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(trustedKeys->trusted(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed.empty()); + } + { + // List threshold = 1 + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is in UNL + auto const self = valKeys[7]; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 5, // + 0, + 1, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 1); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + + // Expire one publisher - no quorum + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(trustedKeys->trusted(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed.empty()); + } + { + // List threshold = 1 + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 5, // + 0, + 1, + pubManifests, + valManifests, + {}, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 1); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + + // Expire one publisher - no quorum + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(trustedKeys->trusted(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed.empty()); + } + + // Test cases use 2 publishers + { + // List threshold = 1, 1 publisher revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is a random validator + auto const self = randomValidator(); + auto const keysTotal = valKeys.size() + 1; + auto trustedKeys = makeValidatorList( + 2, // + 1, + 1, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 1); + int untrustedCount = 0; + for (auto const& p : publishers) + { + bool const trusted = trustedKeys->trustedPublisher(p.pubKey); + BEAST_EXPECT(p.revoked ^ trusted); + untrustedCount += trusted ? 0 : 1; + } + BEAST_EXPECT(untrustedCount == 1); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + hash_set added; + added.insert(calcNodeID(self.masterPublic)); + 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()); + + // Expire one publisher - no quorum, only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(!trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 1, 1 publisher revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is in UNL + auto const self = valKeys[5]; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 2, // + 1, + 1, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 1); + int untrustedCount = 0; + for (auto const& p : publishers) + { + bool const trusted = trustedKeys->trustedPublisher(p.pubKey); + BEAST_EXPECT(p.revoked ^ trusted); + untrustedCount += trusted ? 0 : 1; + } + BEAST_EXPECT(untrustedCount == 1); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + + // Expire one publisher - no quorum, only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + if (val.masterPublic != self.masterPublic) + { + BEAST_EXPECT(!trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 1, 1 publisher revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 2, // + 1, + 1, + pubManifests, + valManifests, + {}, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 1); + int untrustedCount = 0; + for (auto const& p : publishers) + { + bool const trusted = trustedKeys->trustedPublisher(p.pubKey); + BEAST_EXPECT(p.revoked ^ trusted); + untrustedCount += trusted ? 0 : 1; + } + BEAST_EXPECT(untrustedCount == 1); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + + // Expire one publisher - no quorum, no trusted validators + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 0); + + hash_set removed; + for (auto const& val : valKeys) + { + BEAST_EXPECT(!trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 2 (same as number of trusted publishers) + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is a random validator + auto const self = randomValidator(); + auto const keysTotal = valKeys.size() + 1; + auto trustedKeys = makeValidatorList( + 2, // + 0, + 2, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 2); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + hash_set added; + added.insert(calcNodeID(self.masterPublic)); + 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()); + + // Expire one publisher - only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 2 (same as number of trusted publishers) + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is in UNL + auto const self = valKeys[5]; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 2, // + 0, + 2, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 2); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + hash_set added; + added.insert(calcNodeID(self.masterPublic)); + 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()); + + // Expire one publisher - only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + if (val.masterPublic != self.masterPublic) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + { + // List threshold = 2 (same as number of trusted publishers) + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 2, // + 0, + 2, + pubManifests, + valManifests, + {}, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 2); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + + // Expire one publisher - no trusted validators + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 0); + + hash_set removed; + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + + // Test case for 1 publisher + { + // List threshold = 1 (default), no publisher revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is a random validator + auto const self = randomValidator(); + auto const keysTotal = valKeys.size() + 1; + auto trustedKeys = makeValidatorList( + 1, // + 0, + 0, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 1); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + hash_set added; + added.insert(calcNodeID(self.masterPublic)); + 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()); + + // Expire one publisher - no quorum, only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(!trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + + // Test case for 3 publishers (for 2 is a block above) + { + // List threshold = 2 (default), no publisher revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is in UNL + auto const self = valKeys[2]; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 3, // + 0, + 0, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 2); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + } + + // Test case for 4 publishers + { + // List threshold = 3 (default), no publisher revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 4, // + 0, + 0, + pubManifests, + valManifests, + {}, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 3); + for (auto const& p : publishers) + BEAST_EXPECT(trustedKeys->trustedPublisher(p.pubKey)); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + } + + // Test case for 6 publishers (for 5 is a large block above) + { + // List threshold = 4 (default), 2 publishers revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is a random validator + auto const self = randomValidator(); + auto const keysTotal = valKeys.size() + 1; + auto trustedKeys = makeValidatorList( + 6, // + 2, + 0, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 4); + int untrustedCount = 0; + for (auto const& p : publishers) + { + bool const trusted = trustedKeys->trustedPublisher(p.pubKey); + BEAST_EXPECT(p.revoked ^ trusted); + untrustedCount += trusted ? 0 : 1; + } + BEAST_EXPECT(untrustedCount == 2); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + hash_set added; + added.insert(calcNodeID(self.masterPublic)); + 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()); + + // Expire one publisher - no quorum, only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + + // Test case for 7 publishers + { + // List threshold = 4 (default), 3 publishers revoked + ManifestCache pubManifests; + ManifestCache valManifests; + std::vector publishers; + // Self is in UNL + auto const self = valKeys[2]; + auto const keysTotal = valKeys.size(); + auto trustedKeys = makeValidatorList( + 7, // + 3, + 0, + pubManifests, + valManifests, + self, + publishers); + BEAST_EXPECT(trustedKeys->getListThreshold() == 4); + int untrustedCount = 0; + for (auto const& p : publishers) + { + bool const trusted = trustedKeys->trustedPublisher(p.pubKey); + BEAST_EXPECT(p.revoked ^ trusted); + untrustedCount += trusted ? 0 : 1; + } + BEAST_EXPECT(untrustedCount == 3); + + TrustChanges changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == std::ceil(keysTotal * 0.8f)); + BEAST_EXPECT( + trustedKeys->getTrustedMasterKeys().size() == keysTotal); + + 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()); + + // Expire one publisher - only trusted validator is self + env.timeKeeper().set(publishers.back().expiry); + changes = trustedKeys->updateTrusted( + activeValidators, + env.timeKeeper().now(), + env.app().getOPs(), + env.app().overlay(), + env.app().getHashRouter()); + BEAST_EXPECT(trustedKeys->quorum() == quorumDisabled); + BEAST_EXPECT(trustedKeys->getTrustedMasterKeys().size() == 1); + + hash_set removed; + BEAST_EXPECT(trustedKeys->trusted(self.masterPublic)); + for (auto const& val : valKeys) + { + if (val.masterPublic != self.masterPublic) + { + BEAST_EXPECT(trustedKeys->listed(val.masterPublic)); + BEAST_EXPECT(!trustedKeys->trusted(val.masterPublic)); + removed.insert(calcNodeID(val.masterPublic)); + } + } + BEAST_EXPECT(changes.added.empty()); + BEAST_EXPECT(changes.removed == removed); + } + } + public: void run() override @@ -2403,6 +4140,7 @@ class ValidatorList_test : public beast::unit_test::suite testNegativeUNL(); testSha512Hash(); testBuildMessages(); + testQuorumDisabled(); } }; // namespace test diff --git a/src/test/core/Config_test.cpp b/src/test/core/Config_test.cpp index 3cf77fba2ef..eb646497487 100644 --- a/src/test/core/Config_test.cpp +++ b/src/test/core/Config_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -222,6 +223,9 @@ moreripplevalidators.net [validator_list_keys] 03E74EE14CB525AFBB9F1B7D86CD58ECC4B91452294B42AB4E78F260BD905C091D 030775A669685BD6ABCEBD80385921C7851783D991A8055FD21D2F3966C96F1B56 + +[validator_list_threshold] +2 )rippleConfig"); return configContents; } @@ -538,6 +542,7 @@ nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8 c.loadFromString(toLoad); BEAST_EXPECT(c.legacy("validators_file").empty()); BEAST_EXPECT(c.section(SECTION_VALIDATORS).values().size() == 5); + BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == std::nullopt); } { // load validator list sites and keys from config @@ -549,6 +554,45 @@ trustthesevalidators.gov [validator_list_keys] 021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566 + +[validator_list_threshold] +1 +)rippleConfig"); + c.loadFromString(toLoad); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_SITES).values()[0] == + "ripplevalidators.com"); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_SITES).values()[1] == + "trustthesevalidators.gov"); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 1); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_KEYS).values()[0] == + "021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801" + "E566"); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == + 1); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values()[0] == "1"); + BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == std::size_t(1)); + } + { + // load validator list sites and keys from config + Config c; + std::string toLoad(R"rippleConfig( +[validator_list_sites] +ripplevalidators.com +trustthesevalidators.gov + +[validator_list_keys] +021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566 + +[validator_list_threshold] +0 )rippleConfig"); c.loadFromString(toLoad); BEAST_EXPECT( @@ -565,6 +609,97 @@ trustthesevalidators.gov c.section(SECTION_VALIDATOR_LIST_KEYS).values()[0] == "021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801" "E566"); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == + 1); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values()[0] == "0"); + BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == std::nullopt); + } + { + // load should throw if [validator_list_threshold] is greater than + // the number of [validator_list_keys] + Config c; + std::string toLoad(R"rippleConfig( +[validator_list_sites] +ripplevalidators.com +trustthesevalidators.gov + +[validator_list_keys] +021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566 + +[validator_list_threshold] +2 +)rippleConfig"); + std::string error; + auto const expectedError = + "Value in config section [validator_list_threshold] exceeds " + "the number of configured list keys"; + try + { + c.loadFromString(toLoad); + fail(); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + BEAST_EXPECT(error == expectedError); + } + { + // load should throw if [validator_list_threshold] is malformed + Config c; + std::string toLoad(R"rippleConfig( +[validator_list_sites] +ripplevalidators.com +trustthesevalidators.gov + +[validator_list_keys] +021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566 + +[validator_list_threshold] +value = 2 +)rippleConfig"); + std::string error; + auto const expectedError = + "Config section [validator_list_threshold] should contain " + "single value only"; + try + { + c.loadFromString(toLoad); + fail(); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + BEAST_EXPECT(error == expectedError); + } + { + // load should throw if [validator_list_threshold] is negative + Config c; + std::string toLoad(R"rippleConfig( +[validator_list_sites] +ripplevalidators.com +trustthesevalidators.gov + +[validator_list_keys] +021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566 + +[validator_list_threshold] +-1 +)rippleConfig"); + bool error = false; + try + { + c.loadFromString(toLoad); + fail(); + } + catch (std::bad_cast& e) + { + error = true; + } + BEAST_EXPECT(error); } { // load should throw if [validator_list_sites] is configured but @@ -581,6 +716,7 @@ trustthesevalidators.gov try { c.loadFromString(toLoad); + fail(); } catch (std::runtime_error& e) { @@ -602,6 +738,10 @@ trustthesevalidators.gov c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2); BEAST_EXPECT( c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 2); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == + 1); + BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2); } { // load from specified [validators_file] file name @@ -620,6 +760,10 @@ trustthesevalidators.gov c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2); BEAST_EXPECT( c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 2); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == + 1); + BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2); } { // load from specified [validators_file] relative path @@ -638,6 +782,10 @@ trustthesevalidators.gov c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2); BEAST_EXPECT( c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 2); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == + 1); + BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2); } { // load from validators file in default location @@ -654,6 +802,10 @@ trustthesevalidators.gov c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2); BEAST_EXPECT( c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 2); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == + 1); + BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2); } { // load from specified [validators_file] instead @@ -674,6 +826,10 @@ trustthesevalidators.gov c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2); BEAST_EXPECT( c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 2); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == + 1); + BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2); } { @@ -711,6 +867,39 @@ trustthesevalidators.gov c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 4); BEAST_EXPECT( c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 3); + BEAST_EXPECT( + c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == + 1); + BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2); + } + { + // load should throw if [validator_list_threshold] is present both + // in rippled cfg and validators file + boost::format cc(R"rippleConfig( +[validators_file] +%1% + +[validator_list_threshold] +1 +)rippleConfig"); + std::string error; + detail::ValidatorsTxtGuard const vtg( + *this, "test_cfg", "validators.cfg"); + BEAST_EXPECT(vtg.validatorsFileExists()); + auto const expectedError = + "Config section [validator_list_threshold] should contain " + "single value only"; + try + { + Config c; + c.loadFromString(boost::str(cc % vtg.validatorsFile())); + fail(); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + BEAST_EXPECT(error == expectedError); } { // load should throw if [validators], [validator_keys] and diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index f950e7b6c97..e2d1a1f682c 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -1360,7 +1360,8 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) if (!validators_->load( localSigningKey, config().section(SECTION_VALIDATORS).values(), - config().section(SECTION_VALIDATOR_LIST_KEYS).values())) + config().section(SECTION_VALIDATOR_LIST_KEYS).values(), + config().VALIDATOR_LIST_THRESHOLD)) { JLOG(m_journal.fatal()) << "Invalid entry in validator configuration."; diff --git a/src/xrpld/app/misc/ValidatorList.h b/src/xrpld/app/misc/ValidatorList.h index 543eba2f6b7..7576455798a 100644 --- a/src/xrpld/app/misc/ValidatorList.h +++ b/src/xrpld/app/misc/ValidatorList.h @@ -242,6 +242,9 @@ class ValidatorList // The current list of trusted master keys hash_set trustedMasterKeys_; + // Minimum number of lists on which a trusted validator must appear on + std::size_t listThreshold_; + // The current list of trusted signing keys. For those validators using // a manifest, the signing key is the ephemeral key. For the ones using // a seed, the signing key is the same as the master key. @@ -343,7 +346,8 @@ class ValidatorList load( std::optional const& localSigningKey, std::vector const& configKeys, - std::vector const& publisherKeys); + std::vector const& publisherKeys, + std::optional listThreshold = {}); /** Pull the blob/signature/manifest information out of the appropriate Json body fields depending on the version. @@ -679,6 +683,13 @@ class ValidatorList hash_set getTrustedMasterKeys() const; + /** + * get the validator list threshold + * @return the threshold + */ + std::size_t + getListThreshold() const; + /** * get the master public keys of Negative UNL validators * @return the master public keys diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index ba77a3213c7..3a81443db6b 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -129,6 +129,7 @@ ValidatorList::ValidatorList( , j_(j) , quorum_(minimumQuorum.value_or(1)) // Genesis ledger quorum , minimumQuorum_(minimumQuorum) + , listThreshold_(1) { } @@ -136,7 +137,8 @@ bool ValidatorList::load( std::optional const& localSigningKey, std::vector const& configKeys, - std::vector const& publisherKeys) + std::vector const& publisherKeys, + std::optional listThreshold) { static boost::regex const re( "[[:space:]]*" // skip leading whitespace @@ -190,6 +192,26 @@ ValidatorList::load( ++count; } + if (listThreshold) + { + listThreshold_ = *listThreshold; + // This should be enforced by Config class + XRPL_ASSERT( + listThreshold_ > 0 && listThreshold_ <= publisherLists_.size(), + "ripple::ValidatorList::load : list threshold inside range"); + JLOG(j_.debug()) << "Validator list threshold set in configuration to " + << listThreshold_; + } + else + { + // Want truncated result when dividing an odd integer + listThreshold_ = (publisherLists_.size() < 3) + ? 1 // + : publisherLists_.size() / 2 + 1; + JLOG(j_.debug()) << "Validator list threshold computed as " + << listThreshold_; + } + JLOG(j_.debug()) << "Loaded " << count << " keys"; if (localSigningKey) @@ -197,7 +219,17 @@ ValidatorList::load( // Treat local validator key as though it was listed in the config if (localPubKey_) - keyListings_.insert({*localPubKey_, 1}); + { + // The local validator must meet listThreshold_ so the validator does + // not ignore itself. + auto const [_, inserted] = + keyListings_.insert({*localPubKey_, listThreshold_}); + if (inserted) + { + JLOG(j_.debug()) << "Added own master key " + << toBase58(TokenType::NodePublic, *localPubKey_); + } + } JLOG(j_.debug()) << "Loading configured validator keys"; @@ -227,7 +259,7 @@ ValidatorList::load( if (*id == localPubKey_ || *id == localSigningKey) continue; - auto ret = keyListings_.insert({*id, 1}); + auto ret = keyListings_.insert({*id, listThreshold_}); if (!ret.second) { JLOG(j_.warn()) << "Duplicate node identity: " << match[1]; @@ -1615,6 +1647,8 @@ ValidatorList::getJson() const x[jss::status] = "unknown"; x[jss::expiration] = "unknown"; } + + x[jss::validator_list_threshold] = Json::UInt(listThreshold_); } // Validator keys listed in the local config file @@ -1794,11 +1828,42 @@ ValidatorList::calculateQuorum( return *minimumQuorum_; } - // Do not use achievable quorum until lists from all configured - // publishers are available - for (auto const& list : publisherLists_) + if (!publisherLists_.empty()) { - if (list.second.status != PublisherStatus::available) + // Do not use achievable quorum until lists from a sufficient number of + // configured publishers are available + std::size_t unavailable = 0; + for (auto const& list : publisherLists_) + { + if (list.second.status != PublisherStatus::available) + unavailable += 1; + } + // There are two, subtly different, sides to list threshold: + // + // 1. The minimum required intersection between lists listThreshold_ + // for a validator to be included in trustedMasterKeys_. + // If this many (or more) publishers are unavailable, we are likely + // to NOT include a validator which otherwise would have been used. + // We disable quorum if this happens. + // 2. The minimum number of publishers which, when unavailable, will + // prevent us from hitting the above threshold on ANY validator. + // This is calculated as: + // N - M + 1 + // where + // N: number of publishers i.e. publisherLists_.size() + // M: minimum required intersection i.e. listThreshold_ + // If this happens, we still have this local validator and we do not + // want it to form a quorum of 1, so we disable quorum as well. + // + // We disable quorum if the number of unavailable publishers exceeds + // either of the above thresholds + auto const errorThreshold = std::min( + listThreshold_, // + publisherLists_.size() - listThreshold_ + 1); + XRPL_ASSERT( + errorThreshold > 0, + "ripple::ValidatorList::calculateQuorum : nonzero error threshold"); + if (unavailable >= errorThreshold) return std::numeric_limits::max(); } @@ -1943,20 +2008,27 @@ ValidatorList::updateTrusted( auto it = trustedMasterKeys_.cbegin(); while (it != trustedMasterKeys_.cend()) { - if (!keyListings_.count(*it) || validatorManifests_.revoked(*it)) + auto const kit = keyListings_.find(*it); + if (kit == keyListings_.end() || // + kit->second < listThreshold_ || // + validatorManifests_.revoked(*it)) { trustChanges.removed.insert(calcNodeID(*it)); it = trustedMasterKeys_.erase(it); } else { + XRPL_ASSERT( + kit->second >= listThreshold_, + "ripple::ValidatorList::updateTrusted : count meets threshold"); ++it; } } for (auto const& val : keyListings_) { - if (!validatorManifests_.revoked(val.first) && + if (val.second >= listThreshold_ && + !validatorManifests_.revoked(val.first) && trustedMasterKeys_.emplace(val.first).second) trustChanges.added.insert(calcNodeID(val.first)); } @@ -2036,6 +2108,13 @@ ValidatorList::getTrustedMasterKeys() const return trustedMasterKeys_; } +std::size_t +ValidatorList::getListThreshold() const +{ + std::shared_lock read_lock{mutex_}; + return listThreshold_; +} + hash_set ValidatorList::getNegativeUNL() const { diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index 37a853483d2..4329632b2e2 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -305,6 +305,8 @@ class Config : public BasicConfig std::optional> FORCED_LEDGER_RANGE_PRESENT; + std::optional VALIDATOR_LIST_THRESHOLD; + public: Config(); diff --git a/src/xrpld/core/ConfigSections.h b/src/xrpld/core/ConfigSections.h index 8685d29a4d0..59af2bcf67b 100644 --- a/src/xrpld/core/ConfigSections.h +++ b/src/xrpld/core/ConfigSections.h @@ -91,6 +91,7 @@ struct ConfigSection #define SECTION_VALIDATOR_KEY_REVOCATION "validator_key_revocation" #define SECTION_VALIDATOR_LIST_KEYS "validator_list_keys" #define SECTION_VALIDATOR_LIST_SITES "validator_list_sites" +#define SECTION_VALIDATOR_LIST_THRESHOLD "validator_list_threshold" #define SECTION_VALIDATORS "validators" #define SECTION_VALIDATOR_TOKEN "validator_token" #define SECTION_VETO_AMENDMENTS "veto_amendments" diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index 957ccc767f8..eb7bfcce8a0 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -912,6 +912,13 @@ Config::loadFromString(std::string const& fileContents) if (valListKeys) section(SECTION_VALIDATOR_LIST_KEYS).append(*valListKeys); + auto valListThreshold = + getIniFileSection(iniFile, SECTION_VALIDATOR_LIST_THRESHOLD); + + if (valListThreshold) + section(SECTION_VALIDATOR_LIST_THRESHOLD) + .append(*valListThreshold); + if (!entries && !valKeyEntries && !valListKeys) Throw( "The file specified in [" SECTION_VALIDATORS_FILE @@ -926,6 +933,38 @@ Config::loadFromString(std::string const& fileContents) validatorsFile.string()); } + VALIDATOR_LIST_THRESHOLD = [&]() -> std::optional { + auto const& listThreshold = + section(SECTION_VALIDATOR_LIST_THRESHOLD); + if (listThreshold.lines().empty()) + return std::nullopt; + else if (listThreshold.values().size() == 1) + { + auto strTemp = listThreshold.values()[0]; + auto const listThreshold = + beast::lexicalCastThrow(strTemp); + if (listThreshold == 0) + return std::nullopt; // NOTE: Explicitly ask for computed + else if ( + listThreshold > + section(SECTION_VALIDATOR_LIST_KEYS).values().size()) + { + Throw( + "Value in config section " + "[" SECTION_VALIDATOR_LIST_THRESHOLD + "] exceeds the number of configured list keys"); + } + return listThreshold; + } + else + { + Throw( + "Config section " + "[" SECTION_VALIDATOR_LIST_THRESHOLD + "] should contain single value only"); + } + }(); + // Consolidate [validator_keys] and [validators] section(SECTION_VALIDATORS) .append(section(SECTION_VALIDATOR_KEYS).lines());