#include #include #include #include #include #include #include #include namespace xrpl { namespace test { /* * This file implements the following negative UNL related tests: * -- test filling and applying ttUNL_MODIFY Tx and ledger update * -- 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 ToDisable 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::testable_amendments()); std::vector publicKeys = createPublicKeys(3); // genesis ledger auto l = std::make_shared( create_genesis, env.app().config(), std::vector{}, env.app().getNodeFamily()); // 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(); } }; /** * 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::testable_amendments()), 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_amendment; // So we have different genesis ledgers auto l = std::make_shared( create_genesis, env.app().config(), std::vector{fake_amendment++}, 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->header().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(derivePublicKey(KeyType::ed25519, randomSecretKey())); PublicKey toReEnableKey(derivePublicKey(KeyType::ed25519, randomSecretKey())); 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 = 0; 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()); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) 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->header().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(); } }; BEAST_DEFINE_TESTSUITE(NegativeUNL, consensus, xrpl); BEAST_DEFINE_TESTSUITE(NegativeUNLVoteInternal, consensus, xrpl); BEAST_DEFINE_TESTSUITE_MANUAL(NegativeUNLVoteScoreTable, consensus, xrpl); BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteGoodScore, consensus, xrpl, 1); BEAST_DEFINE_TESTSUITE(NegativeUNLVoteOffline, consensus, xrpl); BEAST_DEFINE_TESTSUITE(NegativeUNLVoteMaxListed, consensus, xrpl); BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteRetiredValidator, consensus, xrpl, 1); BEAST_DEFINE_TESTSUITE(NegativeUNLVoteNewValidator, consensus, xrpl); BEAST_DEFINE_TESTSUITE(NegativeUNLVoteFilterValidations, consensus, xrpl); /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// bool negUnlSizeTest( std::shared_ptr const& l, size_t size, bool hasToDisable, bool hasToReEnable) { bool sameSize = l->negativeUNL().size() == size; bool sameToDisable = (l->validatorToDisable() != std::nullopt) == hasToDisable; bool sameToReEnable = (l->validatorToReEnable() != std::nullopt) == hasToReEnable; return sameSize && sameToDisable && sameToReEnable; } bool applyAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx, bool pass) { auto const res = apply(env.app(), view, tx, ApplyFlags::tapNONE, env.journal); if (pass) return isTesSuccess(res.ter); else return res.ter == tefFAILURE || res.ter == 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 xrpl