//----------------------------------------------------------------------------- /* This file is part of rippled: https://github.com/ripple/rippled Copyright (c) 2020 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 namespace ripple { namespace test { /* * This file implements the following negative UNL related tests: * -- test filling and applying ttUNL_MODIFY Tx and ledger update * -- test ttUNL_MODIFY Tx failure without featureNegativeUNL amendment * -- test the NegativeUNLVote class. The test cases are split to multiple * test classes to allow parallel execution. * -- test the negativeUNLFilter function * * Other negative UNL related tests such as ValidatorList and RPC related ones * are put in their existing unit test files. */ /** * Test the size of the negative UNL in a ledger, * also test if the ledger has ToDisalbe and/or ToReEnable * * @param l the ledger * @param size the expected negative UNL size * @param hasToDisable if expect ToDisable in ledger * @param hasToReEnable if expect ToDisable in ledger * @return true if meet all three expectation */ bool negUnlSizeTest( std::shared_ptr const& l, size_t size, bool hasToDisable, bool hasToReEnable); /** * Try to apply a ttUNL_MODIFY Tx, and test the apply result * * @param env the test environment * @param view the OpenView of the ledger * @param tx the ttUNL_MODIFY Tx * @param pass if the Tx should be applied successfully * @return true if meet the expectation of apply result */ bool applyAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx, bool pass); /** * Verify the content of negative UNL entries (public key and ledger sequence) * of a ledger * * @param l the ledger * @param nUnlLedgerSeq the expected PublicKeys and ledger Sequences * @note nUnlLedgerSeq is copied so that it can be modified. * @return true if meet the expectation */ bool VerifyPubKeyAndSeq( std::shared_ptr const& l, hash_map nUnlLedgerSeq); /** * Count the number of Tx in a TxSet * * @param txSet the TxSet * @return the number of Tx */ std::size_t countTx(std::shared_ptr const& txSet); /** * Create fake public keys * * @param n the number of public keys * @return a vector of public keys created */ std::vector createPublicKeys(std::size_t n); /** * Create ttUNL_MODIFY Tx * * @param disabling disabling or re-enabling a validator * @param seq current ledger seq * @param txKey the public key of the validator * @return the ttUNL_MODIFY Tx */ STTx createTx(bool disabling, LedgerIndex seq, PublicKey const& txKey); class NegativeUNL_test : public beast::unit_test::suite { /** * Test filling and applying ttUNL_MODIFY Tx, as well as ledger update: * * We will build a long history of ledgers, and try to apply different * ttUNL_MODIFY Txes. We will check if the apply results meet expectations * and if the ledgers are updated correctly. */ void testNegativeUNL() { /* * test cases: * * (1) the ledger after genesis * -- cannot apply Disable Tx * -- cannot apply ReEnable Tx * -- nUNL empty * -- no ToDisable * -- no ToReEnable * * (2) a flag ledger * -- apply an Disable Tx * -- cannot apply the second Disable Tx * -- cannot apply a ReEnable Tx * -- nUNL empty * -- has ToDisable with right nodeId * -- no ToReEnable * ++ extra test: first Disable Tx in ledger TxSet * * (3) ledgers before the next flag ledger * -- nUNL empty * -- has ToDisable with right nodeId * -- no ToReEnable * * (4) next flag ledger * -- nUNL size == 1, with right nodeId * -- no ToDisable * -- no ToReEnable * -- cannot apply an Disable Tx with nodeId already in nUNL * -- apply an Disable Tx with different nodeId * -- cannot apply a ReEnable Tx with the same NodeId as Add * -- cannot apply a ReEnable Tx with a NodeId not in nUNL * -- apply a ReEnable Tx with a nodeId already in nUNL * -- has ToDisable with right nodeId * -- has ToReEnable with right nodeId * -- nUNL size still 1, right nodeId * * (5) ledgers before the next flag ledger * -- nUNL size == 1, right nodeId * -- has ToDisable with right nodeId * -- has ToReEnable with right nodeId * * (6) next flag ledger * -- nUNL size == 1, different nodeId * -- no ToDisable * -- no ToReEnable * -- apply an Disable Tx with different nodeId * -- nUNL size still 1, right nodeId * -- has ToDisable with right nodeId * -- no ToReEnable * * (7) ledgers before the next flag ledger * -- nUNL size still 1, right nodeId * -- has ToDisable with right nodeId * -- no ToReEnable * * (8) next flag ledger * -- nUNL size == 2 * -- apply a ReEnable Tx * -- cannot apply second ReEnable Tx, even with right nodeId * -- cannot apply an Disable Tx with the same NodeId as Remove * -- nUNL size == 2 * -- no ToDisable * -- has ToReEnable with right nodeId * * (9) ledgers before the next flag ledger * -- nUNL size == 2 * -- no ToDisable * -- has ToReEnable with right nodeId * * (10) next flag ledger * -- nUNL size == 1 * -- apply a ReEnable Tx * -- nUNL size == 1 * -- no ToDisable * -- has ToReEnable with right nodeId * * (11) ledgers before the next flag ledger * -- nUNL size == 1 * -- no ToDisable * -- has ToReEnable with right nodeId * * (12) next flag ledger * -- nUNL size == 0 * -- no ToDisable * -- no ToReEnable * * (13) ledgers before the next flag ledger * -- nUNL size == 0 * -- no ToDisable * -- no ToReEnable * * (14) next flag ledger * -- nUNL size == 0 * -- no ToDisable * -- no ToReEnable */ testcase("Create UNLModify Tx and apply to ledgers"); jtx::Env env(*this, jtx::supported_amendments() | featureNegativeUNL); std::vector publicKeys = createPublicKeys(3); // genesis ledger auto l = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); BEAST_EXPECT(l->rules().enabled(featureNegativeUNL)); // Record the public keys and ledger sequences of expected negative UNL // validators when we build the ledger history hash_map nUnlLedgerSeq; { //(1) the ledger after genesis, not a flag ledger l = std::make_shared( *l, env.app().timeKeeper().closeTime()); auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]); OpenView accum(&*l); BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false)); BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, false)); accum.apply(*l); BEAST_EXPECT(negUnlSizeTest(l, 0, false, false)); } { //(2) a flag ledger // generate more ledgers for (auto i = 0; i < 256 - 2; ++i) { l = std::make_shared( *l, env.app().timeKeeper().closeTime()); } BEAST_EXPECT(l->isFlagLedger()); l->updateNegativeUNL(); auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); auto txDisable_1 = createTx(true, l->seq(), publicKeys[1]); auto txReEnable_2 = createTx(false, l->seq(), publicKeys[2]); // can apply 1 and only 1 ToDisable Tx, // cannot apply ToReEnable Tx, since negative UNL is empty OpenView accum(&*l); BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, true)); BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_1, false)); BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_2, false)); accum.apply(*l); auto good_size = negUnlSizeTest(l, 0, true, false); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]); //++ first ToDisable Tx in ledger's TxSet uint256 txID = txDisable_0.getTransactionID(); BEAST_EXPECT(l->txExists(txID)); } } { //(3) ledgers before the next flag ledger for (auto i = 0; i < 256; ++i) { auto good_size = negUnlSizeTest(l, 0, true, false); BEAST_EXPECT(good_size); if (good_size) BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]); l = std::make_shared( *l, env.app().timeKeeper().closeTime()); } BEAST_EXPECT(l->isFlagLedger()); l->updateNegativeUNL(); //(4) next flag ledger // test if the ledger updated correctly auto good_size = negUnlSizeTest(l, 1, false, false); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(*(l->negativeUNL().begin()) == publicKeys[0]); nUnlLedgerSeq.emplace(publicKeys[0], l->seq()); } auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); auto txDisable_1 = createTx(true, l->seq(), publicKeys[1]); auto txReEnable_0 = createTx(false, l->seq(), publicKeys[0]); auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]); auto txReEnable_2 = createTx(false, l->seq(), publicKeys[2]); OpenView accum(&*l); BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false)); BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_1, true)); BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, false)); BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_2, false)); BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_0, true)); accum.apply(*l); good_size = negUnlSizeTest(l, 1, true, true); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[0])); BEAST_EXPECT(l->validatorToDisable() == publicKeys[1]); BEAST_EXPECT(l->validatorToReEnable() == publicKeys[0]); // test sfFirstLedgerSequence BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); } } { //(5) ledgers before the next flag ledger for (auto i = 0; i < 256; ++i) { auto good_size = negUnlSizeTest(l, 1, true, true); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[0])); BEAST_EXPECT(l->validatorToDisable() == publicKeys[1]); BEAST_EXPECT(l->validatorToReEnable() == publicKeys[0]); } l = std::make_shared( *l, env.app().timeKeeper().closeTime()); } BEAST_EXPECT(l->isFlagLedger()); l->updateNegativeUNL(); //(6) next flag ledger // test if the ledger updated correctly auto good_size = negUnlSizeTest(l, 1, false, false); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[1])); } auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); OpenView accum(&*l); BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, true)); accum.apply(*l); good_size = negUnlSizeTest(l, 1, true, false); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[1])); BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]); nUnlLedgerSeq.emplace(publicKeys[1], l->seq()); nUnlLedgerSeq.erase(publicKeys[0]); BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); } } { //(7) ledgers before the next flag ledger for (auto i = 0; i < 256; ++i) { auto good_size = negUnlSizeTest(l, 1, true, false); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[1])); BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]); } l = std::make_shared( *l, env.app().timeKeeper().closeTime()); } BEAST_EXPECT(l->isFlagLedger()); l->updateNegativeUNL(); //(8) next flag ledger // test if the ledger updated correctly auto good_size = negUnlSizeTest(l, 2, false, false); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[0])); BEAST_EXPECT(l->negativeUNL().count(publicKeys[1])); nUnlLedgerSeq.emplace(publicKeys[0], l->seq()); BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); } auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); auto txReEnable_0 = createTx(false, l->seq(), publicKeys[0]); auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]); OpenView accum(&*l); BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_0, true)); BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, false)); BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false)); accum.apply(*l); good_size = negUnlSizeTest(l, 2, false, true); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[0])); BEAST_EXPECT(l->negativeUNL().count(publicKeys[1])); BEAST_EXPECT(l->validatorToReEnable() == publicKeys[0]); BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); } } { //(9) ledgers before the next flag ledger for (auto i = 0; i < 256; ++i) { auto good_size = negUnlSizeTest(l, 2, false, true); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[0])); BEAST_EXPECT(l->negativeUNL().count(publicKeys[1])); BEAST_EXPECT(l->validatorToReEnable() == publicKeys[0]); } l = std::make_shared( *l, env.app().timeKeeper().closeTime()); } BEAST_EXPECT(l->isFlagLedger()); l->updateNegativeUNL(); //(10) next flag ledger // test if the ledger updated correctly auto good_size = negUnlSizeTest(l, 1, false, false); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[1])); nUnlLedgerSeq.erase(publicKeys[0]); BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); } auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]); OpenView accum(&*l); BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, true)); accum.apply(*l); good_size = negUnlSizeTest(l, 1, false, true); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[1])); BEAST_EXPECT(l->validatorToReEnable() == publicKeys[1]); BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); } } { //(11) ledgers before the next flag ledger for (auto i = 0; i < 256; ++i) { auto good_size = negUnlSizeTest(l, 1, false, true); BEAST_EXPECT(good_size); if (good_size) { BEAST_EXPECT(l->negativeUNL().count(publicKeys[1])); BEAST_EXPECT(l->validatorToReEnable() == publicKeys[1]); } l = std::make_shared( *l, env.app().timeKeeper().closeTime()); } BEAST_EXPECT(l->isFlagLedger()); l->updateNegativeUNL(); //(12) next flag ledger BEAST_EXPECT(negUnlSizeTest(l, 0, false, false)); } { //(13) ledgers before the next flag ledger for (auto i = 0; i < 256; ++i) { BEAST_EXPECT(negUnlSizeTest(l, 0, false, false)); l = std::make_shared( *l, env.app().timeKeeper().closeTime()); } BEAST_EXPECT(l->isFlagLedger()); l->updateNegativeUNL(); //(14) next flag ledger BEAST_EXPECT(negUnlSizeTest(l, 0, false, false)); } } void run() override { testNegativeUNL(); } }; class NegativeUNLNoAmendment_test : public beast::unit_test::suite { void testNegativeUNLNoAmendment() { testcase("No negative UNL amendment"); jtx::Env env(*this, jtx::supported_amendments() - featureNegativeUNL); std::vector publicKeys = createPublicKeys(1); // genesis ledger auto l = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); BEAST_EXPECT(!l->rules().enabled(featureNegativeUNL)); // generate more ledgers for (auto i = 0; i < 256 - 1; ++i) { l = std::make_shared( *l, env.app().timeKeeper().closeTime()); } BEAST_EXPECT(l->seq() == 256); auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); OpenView accum(&*l); BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false)); accum.apply(*l); BEAST_EXPECT(negUnlSizeTest(l, 0, false, false)); } void run() override { testNegativeUNLNoAmendment(); } }; /** * Utility class for creating validators and ledger history */ struct NetworkHistory { using LedgerHistory = std::vector>; /** * * Only reasonable parameters can be honored, * e.g cannot hasToReEnable when nUNLSize == 0 */ struct Parameter { std::uint32_t numNodes; // number of validators std::uint32_t negUNLSize; // size of negative UNL in the last ledger bool hasToDisable; // if has ToDisable in the last ledger bool hasToReEnable; // if has ToReEnable in the last ledger /** * if not specified, the number of ledgers in the history is calculated * from negUNLSize, hasToDisable, and hasToReEnable */ std::optional numLedgers; }; NetworkHistory(beast::unit_test::suite& suite, Parameter const& p) : env(suite, jtx::supported_amendments() | featureNegativeUNL) , param(p) , validations(env.app().getValidations()) { createNodes(); if (!param.numLedgers) param.numLedgers = 256 * (param.negUNLSize + 1); goodHistory = createLedgerHistory(); } void createNodes() { assert(param.numNodes <= 256); UNLKeys = createPublicKeys(param.numNodes); for (int i = 0; i < param.numNodes; ++i) { UNLKeySet.insert(UNLKeys[i]); UNLNodeIDs.push_back(calcNodeID(UNLKeys[i])); UNLNodeIDSet.insert(UNLNodeIDs.back()); } } /** * create ledger history and apply needed ttUNL_MODIFY tx at flag ledgers * @return */ bool createLedgerHistory() { static uint256 fake_amemdment; // So we have different genesis ledgers auto l = std::make_shared( create_genesis, env.app().config(), std::vector{fake_amemdment++}, env.app().getNodeFamily()); history.push_back(l); // When putting validators into the negative UNL, we start with // validator 0, then validator 1 ... int nidx = 0; while (l->seq() <= param.numLedgers) { l = std::make_shared( *l, env.app().timeKeeper().closeTime()); history.push_back(l); if (l->isFlagLedger()) { l->updateNegativeUNL(); OpenView accum(&*l); if (l->negativeUNL().size() < param.negUNLSize) { auto tx = createTx(true, l->seq(), UNLKeys[nidx]); if (!applyAndTestResult(env, accum, tx, true)) break; ++nidx; } else if (l->negativeUNL().size() == param.negUNLSize) { if (param.hasToDisable) { auto tx = createTx(true, l->seq(), UNLKeys[nidx]); if (!applyAndTestResult(env, accum, tx, true)) break; ++nidx; } if (param.hasToReEnable) { auto tx = createTx(false, l->seq(), UNLKeys[0]); if (!applyAndTestResult(env, accum, tx, true)) break; } } accum.apply(*l); } l->updateSkipList(); } return negUnlSizeTest( l, param.negUNLSize, param.hasToDisable, param.hasToReEnable); } /** * Create a validation * @param ledger the ledger the validation validates * @param v the validator * @return the validation */ std::shared_ptr createSTVal(std::shared_ptr const& ledger, NodeID const& v) { static auto keyPair = randomKeyPair(KeyType::secp256k1); return std::make_shared( env.app().timeKeeper().now(), keyPair.first, keyPair.second, v, [&](STValidation& v) { v.setFieldH256(sfLedgerHash, ledger->info().hash); v.setFieldU32(sfLedgerSequence, ledger->seq()); v.setFlag(vfFullValidation); }); }; /** * Walk the ledger history and create validation messages for the ledgers * * @tparam NeedValidation a function to decided if a validation is needed * @param needVal if a validation is needed for this particular combination * of ledger and validator */ template void walkHistoryAndAddValidations(NeedValidation&& needVal) { std::uint32_t curr = 0; std::size_t need = 256 + 1; // only last 256 + 1 ledgers need validations if (history.size() > need) curr = history.size() - need; for (; curr != history.size(); ++curr) { for (std::size_t i = 0; i < param.numNodes; ++i) { if (needVal(history[curr], i)) { RCLValidation v(createSTVal(history[curr], UNLNodeIDs[i])); v.setTrusted(); validations.add(UNLNodeIDs[i], v); } } } } std::shared_ptr lastLedger() const { return history.back(); } jtx::Env env; Parameter param; RCLValidations& validations; std::vector UNLKeys; hash_set UNLKeySet; std::vector UNLNodeIDs; hash_set UNLNodeIDSet; LedgerHistory history; bool goodHistory; }; auto defaultPreVote = [](NegativeUNLVote& vote) {}; /** * Create a NegativeUNLVote object. It then creates ttUNL_MODIFY Tx as its vote * on negative UNL changes. * * @tparam PreVote a function to be called before vote * @param history the ledger history * @param myId the voting validator * @param expect the number of ttUNL_MODIFY Tx expected * @param pre the PreVote function * @return true if the number of ttUNL_MODIFY Txes created meet expectation */ template bool voteAndCheck( NetworkHistory& history, NodeID const& myId, std::size_t expect, PreVote const& pre = defaultPreVote) { NegativeUNLVote vote(myId, history.env.journal); pre(vote); auto txSet = std::make_shared( SHAMapType::TRANSACTION, history.env.app().getNodeFamily()); vote.doVoting( history.lastLedger(), history.UNLKeySet, history.validations, txSet); return countTx(txSet) == expect; } /** * Test the private member functions of NegativeUNLVote */ class NegativeUNLVoteInternal_test : public beast::unit_test::suite { void testAddTx() { testcase("Create UNLModify Tx"); jtx::Env env(*this); NodeID myId(0xA0); NegativeUNLVote vote(myId, env.journal); // one add, one remove auto txSet = std::make_shared( SHAMapType::TRANSACTION, env.app().getNodeFamily()); PublicKey toDisableKey; PublicKey toReEnableKey; LedgerIndex seq(1234); BEAST_EXPECT(countTx(txSet) == 0); vote.addTx(seq, toDisableKey, NegativeUNLVote::ToDisable, txSet); BEAST_EXPECT(countTx(txSet) == 1); vote.addTx(seq, toReEnableKey, NegativeUNLVote::ToReEnable, txSet); BEAST_EXPECT(countTx(txSet) == 2); // content of a tx is implicitly tested after applied to a ledger // in later test cases } void testPickOneCandidate() { testcase("Pick One Candidate"); jtx::Env env(*this); NodeID myId(0xA0); NegativeUNLVote vote(myId, env.journal); uint256 pad_0(0); uint256 pad_f = ~pad_0; NodeID n_1(1); NodeID n_2(2); NodeID n_3(3); std::vector candidates({n_1}); BEAST_EXPECT(vote.choose(pad_0, candidates) == n_1); BEAST_EXPECT(vote.choose(pad_f, candidates) == n_1); candidates.emplace_back(2); BEAST_EXPECT(vote.choose(pad_0, candidates) == n_1); BEAST_EXPECT(vote.choose(pad_f, candidates) == n_2); candidates.emplace_back(3); BEAST_EXPECT(vote.choose(pad_0, candidates) == n_1); BEAST_EXPECT(vote.choose(pad_f, candidates) == n_3); } void testBuildScoreTableSpecialCases() { testcase("Build Score Table"); /* * 1. no skip list * 2. short skip list * 3. local node not enough history * 4. a node double validated some seq * 5. local node had enough validations but on a wrong chain * 6. a good case, long enough history and perfect scores */ { // 1. no skip list NetworkHistory history = {*this, {10, 0, false, false, 1}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { NegativeUNLVote vote( history.UNLNodeIDs[3], history.env.journal); BEAST_EXPECT(!vote.buildScoreTable( history.lastLedger(), history.UNLNodeIDSet, history.validations)); } } { // 2. short skip list NetworkHistory history = {*this, {10, 0, false, false, 256 / 2}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { NegativeUNLVote vote( history.UNLNodeIDs[3], history.env.journal); BEAST_EXPECT(!vote.buildScoreTable( history.lastLedger(), history.UNLNodeIDSet, history.validations)); } } { // 3. local node not enough history NetworkHistory history = {*this, {10, 0, false, false, 256 + 2}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { NodeID myId = history.UNLNodeIDs[3]; history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { // skip half my validations. return !( history.UNLNodeIDs[idx] == myId && l->seq() % 2 == 0); }); NegativeUNLVote vote(myId, history.env.journal); BEAST_EXPECT(!vote.buildScoreTable( history.lastLedger(), history.UNLNodeIDSet, history.validations)); } } { // 4. a node double validated some seq // 5. local node had enough validations but on a wrong chain NetworkHistory history = {*this, {10, 0, false, false, 256 + 2}}; // We need two chains for these tests bool wrongChainSuccess = history.goodHistory; BEAST_EXPECT(wrongChainSuccess); NetworkHistory::LedgerHistory wrongChain = std::move(history.history); // Create a new chain and use it as the one that majority of nodes // follow history.createLedgerHistory(); BEAST_EXPECT(history.goodHistory); if (history.goodHistory && wrongChainSuccess) { NodeID myId = history.UNLNodeIDs[3]; NodeID badNode = history.UNLNodeIDs[4]; history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { // everyone but me return !(history.UNLNodeIDs[idx] == myId); }); // local node validate wrong chain // a node double validates for (auto& l : wrongChain) { RCLValidation v1(history.createSTVal(l, myId)); history.validations.add(myId, v1); RCLValidation v2(history.createSTVal(l, badNode)); history.validations.add(badNode, v2); } NegativeUNLVote vote(myId, history.env.journal); // local node still on wrong chain, can build a scoreTable, // but all other nodes' scores are zero auto scoreTable = vote.buildScoreTable( wrongChain.back(), history.UNLNodeIDSet, history.validations); BEAST_EXPECT(scoreTable); if (scoreTable) { for (auto const& [n, score] : *scoreTable) { if (n == myId) BEAST_EXPECT(score == 256); else BEAST_EXPECT(score == 0); } } // if local node switched to right history, but cannot build // scoreTable because not enough local validations BEAST_EXPECT(!vote.buildScoreTable( history.lastLedger(), history.UNLNodeIDSet, history.validations)); } } { // 6. a good case NetworkHistory history = {*this, {10, 0, false, false, 256 + 1}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { return true; }); NegativeUNLVote vote( history.UNLNodeIDs[3], history.env.journal); auto scoreTable = vote.buildScoreTable( history.lastLedger(), history.UNLNodeIDSet, history.validations); BEAST_EXPECT(scoreTable); if (scoreTable) { for (auto const& [_, score] : *scoreTable) { (void)_; BEAST_EXPECT(score == 256); } } } } } /** * Find all candidates and check if the number of candidates meets * expectation * * @param vote the NegativeUNLVote object * @param unl the validators * @param negUnl the negative UNL validators * @param scoreTable the score table of validators * @param numDisable number of Disable candidates expected * @param numReEnable number of ReEnable candidates expected * @return true if the number of candidates meets expectation */ bool checkCandidateSizes( NegativeUNLVote& vote, hash_set const& unl, hash_set const& negUnl, hash_map const& scoreTable, std::size_t numDisable, std::size_t numReEnable) { auto [disableCandidates, reEnableCandidates] = vote.findAllCandidates(unl, negUnl, scoreTable); bool rightDisable = disableCandidates.size() == numDisable; bool rightReEnable = reEnableCandidates.size() == numReEnable; return rightDisable && rightReEnable; }; void testFindAllCandidates() { testcase("Find All Candidates"); /* * -- unl size: 35 * -- negUnl size: 3 * * 0. all good scores * 1. all bad scores * 2. all between watermarks * 3. 2 good scorers in negUnl * 4. 2 bad scorers not in negUnl * 5. 2 in negUnl but not in unl, have a remove candidate from score * table * 6. 2 in negUnl but not in unl, no remove candidate from score table * 7. 2 new validators have good scores, already in negUnl * 8. 2 new validators have bad scores, not in negUnl * 9. expired the new validators have bad scores, not in negUnl */ NetworkHistory history = {*this, {35, 0, false, false, 0}}; hash_set negUnl_012; for (std::uint32_t i = 0; i < 3; ++i) negUnl_012.insert(history.UNLNodeIDs[i]); // build a good scoreTable to use, or copy and modify hash_map goodScoreTable; for (auto const& n : history.UNLNodeIDs) goodScoreTable[n] = NegativeUNLVote::negativeUNLHighWaterMark + 1; NegativeUNLVote vote(history.UNLNodeIDs[0], history.env.journal); { // all good scores BEAST_EXPECT(checkCandidateSizes( vote, history.UNLNodeIDSet, negUnl_012, goodScoreTable, 0, 3)); } { // all bad scores hash_map scoreTable; for (auto& n : history.UNLNodeIDs) scoreTable[n] = NegativeUNLVote::negativeUNLLowWaterMark - 1; BEAST_EXPECT(checkCandidateSizes( vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 35 - 3, 0)); } { // all between watermarks hash_map scoreTable; for (auto& n : history.UNLNodeIDs) scoreTable[n] = NegativeUNLVote::negativeUNLLowWaterMark + 1; BEAST_EXPECT(checkCandidateSizes( vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 0, 0)); } { // 2 good scorers in negUnl auto scoreTable = goodScoreTable; scoreTable[*negUnl_012.begin()] = NegativeUNLVote::negativeUNLLowWaterMark + 1; BEAST_EXPECT(checkCandidateSizes( vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 0, 2)); } { // 2 bad scorers not in negUnl auto scoreTable = goodScoreTable; scoreTable[history.UNLNodeIDs[11]] = NegativeUNLVote::negativeUNLLowWaterMark - 1; scoreTable[history.UNLNodeIDs[12]] = NegativeUNLVote::negativeUNLLowWaterMark - 1; BEAST_EXPECT(checkCandidateSizes( vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 2, 3)); } { // 2 in negUnl but not in unl, have a remove candidate from score // table hash_set UNL_temp = history.UNLNodeIDSet; UNL_temp.erase(history.UNLNodeIDs[0]); UNL_temp.erase(history.UNLNodeIDs[1]); BEAST_EXPECT(checkCandidateSizes( vote, UNL_temp, negUnl_012, goodScoreTable, 0, 3)); } { // 2 in negUnl but not in unl, no remove candidate from score table auto scoreTable = goodScoreTable; scoreTable.erase(history.UNLNodeIDs[0]); scoreTable.erase(history.UNLNodeIDs[1]); scoreTable[history.UNLNodeIDs[2]] = NegativeUNLVote::negativeUNLLowWaterMark + 1; hash_set UNL_temp = history.UNLNodeIDSet; UNL_temp.erase(history.UNLNodeIDs[0]); UNL_temp.erase(history.UNLNodeIDs[1]); BEAST_EXPECT(checkCandidateSizes( vote, UNL_temp, negUnl_012, scoreTable, 0, 2)); } { // 2 new validators NodeID new_1(0xbead); NodeID new_2(0xbeef); hash_set nowTrusted = {new_1, new_2}; hash_set UNL_temp = history.UNLNodeIDSet; UNL_temp.insert(new_1); UNL_temp.insert(new_2); vote.newValidators(256, nowTrusted); { // 2 new validators have good scores, already in negUnl auto scoreTable = goodScoreTable; scoreTable[new_1] = NegativeUNLVote::negativeUNLHighWaterMark + 1; scoreTable[new_2] = NegativeUNLVote::negativeUNLHighWaterMark + 1; hash_set negUnl_temp = negUnl_012; negUnl_temp.insert(new_1); negUnl_temp.insert(new_2); BEAST_EXPECT(checkCandidateSizes( vote, UNL_temp, negUnl_temp, scoreTable, 0, 3 + 2)); } { // 2 new validators have bad scores, not in negUnl auto scoreTable = goodScoreTable; scoreTable[new_1] = 0; scoreTable[new_2] = 0; BEAST_EXPECT(checkCandidateSizes( vote, UNL_temp, negUnl_012, scoreTable, 0, 3)); } { // expired the new validators have bad scores, not in negUnl vote.purgeNewValidators( 256 + NegativeUNLVote::newValidatorDisableSkip + 1); auto scoreTable = goodScoreTable; scoreTable[new_1] = 0; scoreTable[new_2] = 0; BEAST_EXPECT(checkCandidateSizes( vote, UNL_temp, negUnl_012, scoreTable, 2, 3)); } } } void testFindAllCandidatesCombination() { testcase("Find All Candidates Combination"); /* * == combination 1: * -- unl size: 34, 35, 80 * -- nUnl size: 0, 50%, all * -- score pattern: all 0, all negativeUNLLowWaterMark & +1 & -1, all * negativeUNLHighWaterMark & +1 & -1, all 100% * * == combination 2: * -- unl size: 34, 35, 80 * -- negativeUNL size: 0, all * -- nUnl size: one on, one off, one on, one off, * -- score pattern: 2*(negativeUNLLowWaterMark, +1, -1) & * 2*(negativeUNLHighWaterMark, +1, -1) & rest * negativeUNLMinLocalValsToVote */ jtx::Env env(*this); NodeID myId(0xA0); NegativeUNLVote vote(myId, env.journal); std::array unlSizes = {34, 35, 80}; std::array nUnlPercent = {0, 50, 100}; std::array scores = { 0, NegativeUNLVote::negativeUNLLowWaterMark - 1, NegativeUNLVote::negativeUNLLowWaterMark, NegativeUNLVote::negativeUNLLowWaterMark + 1, NegativeUNLVote::negativeUNLHighWaterMark - 1, NegativeUNLVote::negativeUNLHighWaterMark, NegativeUNLVote::negativeUNLHighWaterMark + 1, NegativeUNLVote::negativeUNLMinLocalValsToVote}; //== combination 1: { auto fillScoreTable = [&](std::uint32_t unl_size, std::uint32_t nUnl_size, std::uint32_t score, hash_set& unl, hash_set& negUnl, hash_map& scoreTable) { std::vector nodeIDs; std::vector keys = createPublicKeys(unl_size); for (auto const& k : keys) { nodeIDs.emplace_back(calcNodeID(k)); unl.emplace(nodeIDs.back()); scoreTable[nodeIDs.back()] = score; } for (std::uint32_t i = 0; i < nUnl_size; ++i) negUnl.insert(nodeIDs[i]); }; for (auto us : unlSizes) { for (auto np : nUnlPercent) { for (auto score : scores) { hash_set unl; hash_set negUnl; hash_map scoreTable; fillScoreTable( us, us * np / 100, score, unl, negUnl, scoreTable); BEAST_EXPECT(unl.size() == us); BEAST_EXPECT(negUnl.size() == us * np / 100); BEAST_EXPECT(scoreTable.size() == us); std::size_t toDisable_expect = 0; std::size_t toReEnable_expect = 0; if (np == 0) { if (score < NegativeUNLVote::negativeUNLLowWaterMark) { toDisable_expect = us; } } else if (np == 50) { if (score > NegativeUNLVote::negativeUNLHighWaterMark) { toReEnable_expect = us * np / 100; } } else { if (score > NegativeUNLVote::negativeUNLHighWaterMark) { toReEnable_expect = us; } } BEAST_EXPECT(checkCandidateSizes( vote, unl, negUnl, scoreTable, toDisable_expect, toReEnable_expect)); } } } //== combination 2: { auto fillScoreTable = [&](std::uint32_t unl_size, std::uint32_t nUnl_percent, hash_set& unl, hash_set& negUnl, hash_map& scoreTable) { std::vector nodeIDs; std::vector keys = createPublicKeys(unl_size); for (auto const& k : keys) { nodeIDs.emplace_back(calcNodeID(k)); unl.emplace(nodeIDs.back()); } std::uint32_t nIdx = 0; for (auto score : scores) { scoreTable[nodeIDs[nIdx++]] = score; scoreTable[nodeIDs[nIdx++]] = score; } for (; nIdx < unl_size;) { scoreTable[nodeIDs[nIdx++]] = scores.back(); } if (nUnl_percent == 100) { negUnl = unl; } else if (nUnl_percent == 50) { for (std::uint32_t i = 1; i < unl_size; i += 2) negUnl.insert(nodeIDs[i]); } }; for (auto us : unlSizes) { for (auto np : nUnlPercent) { hash_set unl; hash_set negUnl; hash_map scoreTable; fillScoreTable(us, np, unl, negUnl, scoreTable); BEAST_EXPECT(unl.size() == us); BEAST_EXPECT(negUnl.size() == us * np / 100); BEAST_EXPECT(scoreTable.size() == us); std::size_t toDisable_expect = 0; std::size_t toReEnable_expect = 0; if (np == 0) { toDisable_expect = 4; } else if (np == 50) { toReEnable_expect = negUnl.size() - 6; } else { toReEnable_expect = negUnl.size() - 12; } BEAST_EXPECT(checkCandidateSizes( vote, unl, negUnl, scoreTable, toDisable_expect, toReEnable_expect)); } } } } } void testNewValidators() { testcase("New Validators"); jtx::Env env(*this); NodeID myId(0xA0); NegativeUNLVote vote(myId, env.journal); // test cases: // newValidators_ of the NegativeUNLVote empty, add one // add a new one and one already added // add a new one and some already added // purge and see some are expired NodeID n1(0xA1); NodeID n2(0xA2); NodeID n3(0xA3); vote.newValidators(2, {n1}); BEAST_EXPECT(vote.newValidators_.size() == 1); if (vote.newValidators_.size() == 1) { BEAST_EXPECT(vote.newValidators_.begin()->first == n1); BEAST_EXPECT(vote.newValidators_.begin()->second == 2); } vote.newValidators(3, {n1, n2}); BEAST_EXPECT(vote.newValidators_.size() == 2); if (vote.newValidators_.size() == 2) { BEAST_EXPECT(vote.newValidators_[n1] == 2); BEAST_EXPECT(vote.newValidators_[n2] == 3); } vote.newValidators( NegativeUNLVote::newValidatorDisableSkip, {n1, n2, n3}); BEAST_EXPECT(vote.newValidators_.size() == 3); if (vote.newValidators_.size() == 3) { BEAST_EXPECT(vote.newValidators_[n1] == 2); BEAST_EXPECT(vote.newValidators_[n2] == 3); BEAST_EXPECT( vote.newValidators_[n3] == NegativeUNLVote::newValidatorDisableSkip); } vote.purgeNewValidators(NegativeUNLVote::newValidatorDisableSkip + 2); BEAST_EXPECT(vote.newValidators_.size() == 3); vote.purgeNewValidators(NegativeUNLVote::newValidatorDisableSkip + 3); BEAST_EXPECT(vote.newValidators_.size() == 2); vote.purgeNewValidators(NegativeUNLVote::newValidatorDisableSkip + 4); BEAST_EXPECT(vote.newValidators_.size() == 1); BEAST_EXPECT(vote.newValidators_.begin()->first == n3); BEAST_EXPECT( vote.newValidators_.begin()->second == NegativeUNLVote::newValidatorDisableSkip); } void run() override { testAddTx(); testPickOneCandidate(); testBuildScoreTableSpecialCases(); testFindAllCandidates(); testFindAllCandidatesCombination(); testNewValidators(); } }; /** * Rest the build score table function of NegativeUNLVote. * This was a part of NegativeUNLVoteInternal. It is redundant and has long * runtime. So we separate it out as a manual test. */ class NegativeUNLVoteScoreTable_test : public beast::unit_test::suite { void testBuildScoreTableCombination() { testcase("Build Score Table Combination"); /* * local node good history, correct scores: * == combination: * -- unl size: 10, 34, 35, 50 * -- score pattern: all 0, all 50%, all 100%, two 0% two 50% rest 100% */ std::array unlSizes = {10, 34, 35, 50}; std::array, 4> scorePattern = { {{{0, 0, 0}}, {{50, 50, 50}}, {{100, 100, 100}}, {{0, 50, 100}}}}; for (auto unlSize : unlSizes) { for (std::uint32_t sp = 0; sp < 4; ++sp) { NetworkHistory history = { *this, {unlSize, 0, false, false, 256 + 2}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { NodeID myId = history.UNLNodeIDs[3]; history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { std::size_t k; if (idx < 2) k = 0; else if (idx < 4) k = 1; else k = 2; bool add_50 = scorePattern[sp][k] == 50 && l->seq() % 2 == 0; bool add_100 = scorePattern[sp][k] == 100; bool add_me = history.UNLNodeIDs[idx] == myId; return add_50 || add_100 || add_me; }); NegativeUNLVote vote(myId, history.env.journal); auto scoreTable = vote.buildScoreTable( history.lastLedger(), history.UNLNodeIDSet, history.validations); BEAST_EXPECT(scoreTable); if (scoreTable) { std::uint32_t i = 0; // looping unl auto checkScores = [&](std::uint32_t score, std::uint32_t k) -> bool { if (history.UNLNodeIDs[i] == myId) return score == 256; if (scorePattern[sp][k] == 0) return score == 0; if (scorePattern[sp][k] == 50) return score == 256 / 2; if (scorePattern[sp][k] == 100) return score == 256; else return false; }; for (; i < 2; ++i) { BEAST_EXPECT(checkScores( (*scoreTable)[history.UNLNodeIDs[i]], 0)); } for (; i < 4; ++i) { BEAST_EXPECT(checkScores( (*scoreTable)[history.UNLNodeIDs[i]], 1)); } for (; i < unlSize; ++i) { BEAST_EXPECT(checkScores( (*scoreTable)[history.UNLNodeIDs[i]], 2)); } } } } } } void run() override { testBuildScoreTableCombination(); } }; /* * Test the doVoting function of NegativeUNLVote. * The test cases are split to 5 classes for parallel execution. * * Voting tests: (use hasToDisable and hasToReEnable in some of the cases) * * == all good score, nUnl empty * -- txSet.size = 0 * == all good score, nUnl not empty (use hasToDisable) * -- txSet.size = 1 * * == 2 nodes offline, nUnl empty (use hasToReEnable) * -- txSet.size = 1 * == 2 nodes offline, in nUnl * -- txSet.size = 0 * * == 2 nodes offline, not in nUnl, but maxListed * -- txSet.size = 0 * * == 2 nodes offline including me, not in nUnl * -- txSet.size = 0 * == 2 nodes offline, not in negativeUNL, but I'm not a validator * -- txSet.size = 0 * == 2 in nUnl, but not in unl, no other remove candidates * -- txSet.size = 1 * * == 2 new validators have bad scores * -- txSet.size = 0 * == 2 expired new validators have bad scores * -- txSet.size = 1 */ class NegativeUNLVoteGoodScore_test : public beast::unit_test::suite { void testDoVoting() { testcase("Do Voting"); { //== all good score, negativeUNL empty //-- txSet.size = 0 NetworkHistory history = {*this, {51, 0, false, false, {}}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { return true; }); BEAST_EXPECT(voteAndCheck(history, history.UNLNodeIDs[0], 0)); } } { // all good score, negativeUNL not empty (use hasToDisable) //-- txSet.size = 1 NetworkHistory history = {*this, {37, 0, true, false, {}}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { return true; }); BEAST_EXPECT(voteAndCheck(history, history.UNLNodeIDs[0], 1)); } } } void run() override { testDoVoting(); } }; class NegativeUNLVoteOffline_test : public beast::unit_test::suite { void testDoVoting() { testcase("Do Voting"); { //== 2 nodes offline, negativeUNL empty (use hasToReEnable) //-- txSet.size = 1 NetworkHistory history = {*this, {29, 1, false, true, {}}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { // skip node 0 and node 1 return idx > 1; }); BEAST_EXPECT( voteAndCheck(history, history.UNLNodeIDs.back(), 1)); } } { // 2 nodes offline, in negativeUNL //-- txSet.size = 0 NetworkHistory history = {*this, {30, 1, true, false, {}}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { NodeID n1 = calcNodeID(*history.lastLedger()->negativeUNL().begin()); NodeID n2 = calcNodeID(*history.lastLedger()->validatorToDisable()); history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { // skip node 0 and node 1 return history.UNLNodeIDs[idx] != n1 && history.UNLNodeIDs[idx] != n2; }); BEAST_EXPECT( voteAndCheck(history, history.UNLNodeIDs.back(), 0)); } } } void run() override { testDoVoting(); } }; class NegativeUNLVoteMaxListed_test : public beast::unit_test::suite { void testDoVoting() { testcase("Do Voting"); { // 2 nodes offline, not in negativeUNL, but maxListed //-- txSet.size = 0 NetworkHistory history = {*this, {32, 8, true, true, {}}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { // skip node 0 ~ 10 return idx > 10; }); BEAST_EXPECT( voteAndCheck(history, history.UNLNodeIDs.back(), 0)); } } } void run() override { testDoVoting(); } }; class NegativeUNLVoteRetiredValidator_test : public beast::unit_test::suite { void testDoVoting() { testcase("Do Voting"); { //== 2 nodes offline including me, not in negativeUNL //-- txSet.size = 0 NetworkHistory history = {*this, {35, 0, false, false, {}}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { return idx > 1; }); BEAST_EXPECT(voteAndCheck(history, history.UNLNodeIDs[0], 0)); } } { // 2 nodes offline, not in negativeUNL, but I'm not a validator //-- txSet.size = 0 NetworkHistory history = {*this, {40, 0, false, false, {}}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { return idx > 1; }); BEAST_EXPECT(voteAndCheck(history, NodeID(0xdeadbeef), 0)); } } { //== 2 in negativeUNL, but not in unl, no other remove candidates //-- txSet.size = 1 NetworkHistory history = {*this, {25, 2, false, false, {}}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { return idx > 1; }); BEAST_EXPECT(voteAndCheck( history, history.UNLNodeIDs.back(), 1, [&](NegativeUNLVote& vote) { history.UNLKeySet.erase(history.UNLKeys[0]); history.UNLKeySet.erase(history.UNLKeys[1]); })); } } } void run() override { testDoVoting(); } }; class NegativeUNLVoteNewValidator_test : public beast::unit_test::suite { void testDoVoting() { testcase("Do Voting"); { //== 2 new validators have bad scores //-- txSet.size = 0 NetworkHistory history = {*this, {15, 0, false, false, {}}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { return true; }); BEAST_EXPECT(voteAndCheck( history, history.UNLNodeIDs[0], 0, [&](NegativeUNLVote& vote) { auto extra_key_1 = randomKeyPair(KeyType::ed25519).first; auto extra_key_2 = randomKeyPair(KeyType::ed25519).first; history.UNLKeySet.insert(extra_key_1); history.UNLKeySet.insert(extra_key_2); hash_set nowTrusted; nowTrusted.insert(calcNodeID(extra_key_1)); nowTrusted.insert(calcNodeID(extra_key_2)); vote.newValidators( history.lastLedger()->seq(), nowTrusted); })); } } { //== 2 expired new validators have bad scores //-- txSet.size = 1 NetworkHistory history = { *this, {21, 0, false, false, NegativeUNLVote::newValidatorDisableSkip * 2}}; BEAST_EXPECT(history.goodHistory); if (history.goodHistory) { history.walkHistoryAndAddValidations( [&](std::shared_ptr const& l, std::size_t idx) -> bool { return true; }); BEAST_EXPECT(voteAndCheck( history, history.UNLNodeIDs[0], 1, [&](NegativeUNLVote& vote) { auto extra_key_1 = randomKeyPair(KeyType::ed25519).first; auto extra_key_2 = randomKeyPair(KeyType::ed25519).first; history.UNLKeySet.insert(extra_key_1); history.UNLKeySet.insert(extra_key_2); hash_set nowTrusted; nowTrusted.insert(calcNodeID(extra_key_1)); nowTrusted.insert(calcNodeID(extra_key_2)); vote.newValidators(256, nowTrusted); })); } } } void run() override { testDoVoting(); } }; class NegativeUNLVoteFilterValidations_test : public beast::unit_test::suite { void testFilterValidations() { testcase("Filter Validations"); jtx::Env env(*this); auto l = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); auto createSTVal = [&](std::pair const& keys) { return std::make_shared( env.app().timeKeeper().now(), keys.first, keys.second, calcNodeID(keys.first), [&](STValidation& v) { v.setFieldH256(sfLedgerHash, l->info().hash); v.setFieldU32(sfLedgerSequence, l->seq()); v.setFlag(vfFullValidation); }); }; // create keys and validations std::uint32_t numNodes = 10; std::uint32_t negUnlSize = 3; std::vector cfgKeys; hash_set activeValidators; hash_set nUnlKeys; std::vector> vals; for (int i = 0; i < numNodes; ++i) { auto keyPair = randomKeyPair(KeyType::secp256k1); vals.emplace_back(createSTVal(keyPair)); cfgKeys.push_back(toBase58(TokenType::NodePublic, keyPair.first)); activeValidators.emplace(calcNodeID(keyPair.first)); if (i < negUnlSize) { nUnlKeys.insert(keyPair.first); } } // setup the ValidatorList auto& validators = env.app().validators(); auto& local = *nUnlKeys.begin(); std::vector cfgPublishers; validators.load(local, cfgKeys, cfgPublishers); validators.updateTrusted( activeValidators, env.timeKeeper().now(), env.app().getOPs(), env.app().overlay(), env.app().getHashRouter()); BEAST_EXPECT(validators.getTrustedMasterKeys().size() == numNodes); validators.setNegativeUNL(nUnlKeys); BEAST_EXPECT(validators.getNegativeUNL().size() == negUnlSize); // test the filter BEAST_EXPECT(vals.size() == numNodes); vals = validators.negativeUNLFilter(std::move(vals)); BEAST_EXPECT(vals.size() == numNodes - negUnlSize); } void run() override { testFilterValidations(); } }; class NegativeUNLgRPC_test : public beast::unit_test::suite { template std::string toByteString(T const& data) { const char* bytes = reinterpret_cast(data.data()); return {bytes, data.size()}; } void testGRPC() { testcase("gRPC test"); auto gRpcTest = [this]( std::uint32_t negUnlSize, bool hasToDisable, bool hasToReEnable) -> bool { NetworkHistory history = { *this, {20, negUnlSize, hasToDisable, hasToReEnable, {}}}; if (!history.goodHistory) return false; auto const& negUnlObject = history.lastLedger()->read(keylet::negativeUNL()); if (!negUnlSize && !hasToDisable && !hasToReEnable && !negUnlObject) return true; if (!negUnlObject) return false; org::xrpl::rpc::v1::NegativeUNL to; ripple::RPC::convert(to, *negUnlObject); if (!to.has_flags() || to.flags().value() != negUnlObject->getFlags()) return false; bool goodSize = to.disabled_validators_size() == negUnlSize && to.has_validator_to_disable() == hasToDisable && to.has_validator_to_re_enable() == hasToReEnable; if (!goodSize) return false; if (negUnlSize) { if (!negUnlObject->isFieldPresent(sfDisabledValidators)) return false; auto const& nUnlData = negUnlObject->getFieldArray(sfDisabledValidators); if (nUnlData.size() != negUnlSize) return false; int idx = 0; for (auto const& n : nUnlData) { if (!n.isFieldPresent(sfPublicKey) || !n.isFieldPresent(sfFirstLedgerSequence)) return false; if (!to.disabled_validators(idx).has_ledger_sequence() || !to.disabled_validators(idx).has_public_key()) return false; if (to.disabled_validators(idx).public_key().value() != toByteString(n.getFieldVL(sfPublicKey))) return false; if (to.disabled_validators(idx).ledger_sequence().value() != n.getFieldU32(sfFirstLedgerSequence)) return false; ++idx; } } if (hasToDisable) { if (!negUnlObject->isFieldPresent(sfValidatorToDisable)) return false; if (to.validator_to_disable().value() != toByteString( negUnlObject->getFieldVL(sfValidatorToDisable))) return false; } if (hasToReEnable) { if (!negUnlObject->isFieldPresent(sfValidatorToReEnable)) return false; if (to.validator_to_re_enable().value() != toByteString( negUnlObject->getFieldVL(sfValidatorToReEnable))) return false; } return true; }; BEAST_EXPECT(gRpcTest(0, false, false)); BEAST_EXPECT(gRpcTest(2, true, true)); } void run() override { testGRPC(); } }; BEAST_DEFINE_TESTSUITE(NegativeUNL, ledger, ripple); BEAST_DEFINE_TESTSUITE(NegativeUNLNoAmendment, ledger, ripple); BEAST_DEFINE_TESTSUITE(NegativeUNLVoteInternal, consensus, ripple); BEAST_DEFINE_TESTSUITE_MANUAL(NegativeUNLVoteScoreTable, consensus, ripple); BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteGoodScore, consensus, ripple, 1); BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteOffline, consensus, ripple, 1); BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteMaxListed, consensus, ripple, 1); BEAST_DEFINE_TESTSUITE_PRIO( NegativeUNLVoteRetiredValidator, consensus, ripple, 1); BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteNewValidator, consensus, ripple, 1); BEAST_DEFINE_TESTSUITE(NegativeUNLVoteFilterValidations, consensus, ripple); BEAST_DEFINE_TESTSUITE(NegativeUNLgRPC, ledger, ripple); /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// bool negUnlSizeTest( std::shared_ptr const& l, size_t size, bool hasToDisable, bool hasToReEnable) { bool sameSize = l->negativeUNL().size() == size; bool sameToDisable = (l->validatorToDisable() != boost::none) == hasToDisable; bool sameToReEnable = (l->validatorToReEnable() != boost::none) == hasToReEnable; return sameSize && sameToDisable && sameToReEnable; } bool applyAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx, bool pass) { auto res = apply(env.app(), view, tx, ApplyFlags::tapNONE, env.journal); if (pass) return res.first == tesSUCCESS; else return res.first == tefFAILURE || res.first == temDISABLED; } bool VerifyPubKeyAndSeq( std::shared_ptr const& l, hash_map nUnlLedgerSeq) { auto sle = l->read(keylet::negativeUNL()); if (!sle) return false; if (!sle->isFieldPresent(sfDisabledValidators)) return false; auto const& nUnlData = sle->getFieldArray(sfDisabledValidators); if (nUnlData.size() != nUnlLedgerSeq.size()) return false; for (auto const& n : nUnlData) { if (!n.isFieldPresent(sfFirstLedgerSequence) || !n.isFieldPresent(sfPublicKey)) return false; auto seq = n.getFieldU32(sfFirstLedgerSequence); auto d = n.getFieldVL(sfPublicKey); auto s = makeSlice(d); if (!publicKeyType(s)) return false; PublicKey pk(s); auto it = nUnlLedgerSeq.find(pk); if (it == nUnlLedgerSeq.end()) return false; if (it->second != seq) return false; nUnlLedgerSeq.erase(it); } return nUnlLedgerSeq.size() == 0; } std::size_t countTx(std::shared_ptr const& txSet) { std::size_t count = 0; for (auto i = txSet->begin(); i != txSet->end(); ++i) { ++count; } return count; }; std::vector createPublicKeys(std::size_t n) { std::vector keys; std::size_t ss = 33; std::vector data(ss, 0); data[0] = 0xED; for (int i = 0; i < n; ++i) { data[1]++; Slice s(data.data(), ss); keys.emplace_back(s); } return keys; } STTx createTx(bool disabling, LedgerIndex seq, PublicKey const& txKey) { auto fill = [&](auto& obj) { obj.setFieldU8(sfUNLModifyDisabling, disabling ? 1 : 0); obj.setFieldU32(sfLedgerSequence, seq); obj.setFieldVL(sfUNLModifyValidator, txKey); }; return STTx(ttUNL_MODIFY, fill); } } // namespace test } // namespace ripple