Files
rippled/src/test/consensus/NegativeUNL_test.cpp
Mayukha Vadari dcaef828b4 refactor: Replace !=/== tesSuccess with using isTesSuccess (#6409)
This change replaces all instances of `<variable> != tesSUCCESS` with `!isTesSuccess(<variable>)` and `<variable> == tesSUCCESS` with `isTesSuccess(<variable>)`.
2026-03-18 14:15:10 +00:00

1837 lines
66 KiB
C++

#include <test/jtx.h>
#include <xrpld/app/consensus/RCLValidations.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/misc/NegativeUNLVote.h>
#include <xrpld/app/misc/ValidatorList.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/ledger/View.h>
#include <xrpl/tx/apply.h>
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<Ledger const> 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<Ledger const> const& l,
hash_map<PublicKey, std::uint32_t> 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<SHAMap> const& txSet);
/**
* Create fake public keys
*
* @param n the number of public keys
* @return a vector of public keys created
*/
std::vector<PublicKey>
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<PublicKey> publicKeys = createPublicKeys(3);
// genesis ledger
auto l = std::make_shared<Ledger>(
create_genesis, env.app().config(), std::vector<uint256>{}, env.app().getNodeFamily());
// Record the public keys and ledger sequences of expected negative UNL
// validators when we build the ledger history
hash_map<PublicKey, std::uint32_t> nUnlLedgerSeq;
{
//(1) the ledger after genesis, not a flag ledger
l = std::make_shared<Ledger>(*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<Ledger>(*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<Ledger>(*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<Ledger>(*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<Ledger>(*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<Ledger>(*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<Ledger>(*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<Ledger>(*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<std::shared_ptr<Ledger>>;
/**
*
* 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<int> 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<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{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<Ledger>(*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<STValidation>
createSTVal(std::shared_ptr<Ledger const> const& ledger, NodeID const& v)
{
static auto keyPair = randomKeyPair(KeyType::secp256k1);
return std::make_shared<STValidation>(
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 <class NeedValidation>
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<Ledger const>
lastLedger() const
{
return history.back();
}
jtx::Env env;
Parameter param;
RCLValidations& validations;
std::vector<PublicKey> UNLKeys;
hash_set<PublicKey> UNLKeySet;
std::vector<NodeID> UNLNodeIDs;
hash_set<NodeID> 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 <typename PreVote = decltype(defaultPreVote)>
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<SHAMap>(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<SHAMap>(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<NodeID> 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<Ledger const> 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<Ledger const> 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<Ledger const> 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<NodeID> const& unl,
hash_set<NodeID> const& negUnl,
hash_map<NodeID, std::uint32_t> 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<NodeID> 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<NodeID, std::uint32_t> 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<NodeID, std::uint32_t> 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<NodeID, std::uint32_t> 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<NodeID> 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<NodeID> 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<NodeID> nowTrusted = {new_1, new_2};
hash_set<NodeID> 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<NodeID> 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<std::uint32_t, 3> unlSizes = {34, 35, 80};
std::array<std::uint32_t, 3> nUnlPercent = {0, 50, 100};
std::array<std::uint32_t, 8> 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<NodeID>& unl,
hash_set<NodeID>& negUnl,
hash_map<NodeID, std::uint32_t>& scoreTable) {
std::vector<NodeID> nodeIDs;
std::vector<PublicKey> 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<NodeID> unl;
hash_set<NodeID> negUnl;
hash_map<NodeID, std::uint32_t> 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<NodeID>& unl,
hash_set<NodeID>& negUnl,
hash_map<NodeID, std::uint32_t>& scoreTable) {
std::vector<NodeID> nodeIDs;
std::vector<PublicKey> 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<NodeID> unl;
hash_set<NodeID> negUnl;
hash_map<NodeID, std::uint32_t> 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<std::uint32_t, 4> unlSizes = {10, 34, 35, 50};
std::array<std::array<std::uint32_t, 3>, 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<Ledger const> 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<Ledger const> 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<Ledger const> 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<Ledger const> 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<Ledger const> 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<Ledger const> 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<Ledger const> 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<Ledger const> 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<Ledger const> 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<Ledger const> 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<NodeID> 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<Ledger const> 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<NodeID> 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<Ledger>(
create_genesis, env.app().config(), std::vector<uint256>{}, env.app().getNodeFamily());
auto createSTVal = [&](std::pair<PublicKey, SecretKey> const& keys) {
return std::make_shared<STValidation>(
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<std::string> cfgKeys;
hash_set<NodeID> activeValidators;
hash_set<PublicKey> nUnlKeys;
std::vector<std::shared_ptr<STValidation>> 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<std::string> 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<Ledger const> 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<Ledger const> const& l,
hash_map<PublicKey, std::uint32_t> 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<SHAMap> const& txSet)
{
std::size_t count = 0;
for (auto i = txSet->begin(); i != txSet->end(); ++i)
{
++count;
}
return count;
};
std::vector<PublicKey>
createPublicKeys(std::size_t n)
{
std::vector<PublicKey> keys;
std::size_t ss = 33;
std::vector<uint8_t> 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