Files
rippled/src/test/consensus/NegativeUNL_test.cpp

1989 lines
70 KiB
C++

#include <test/jtx/Env.h>
#include <xrpld/app/consensus/RCLValidations.h>
#include <xrpld/app/misc/NegativeUNLVote.h>
#include <xrpld/app/misc/ValidatorList.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STValidation.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/tokens.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/tx/apply.h>
#include <array>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
namespace xrpl::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::testableAmendments());
std::vector<PublicKey> publicKeys = createPublicKeys(3);
// genesis ledger
auto l = std::make_shared<Ledger>(
kCREATE_GENESIS,
Rules{env.app().config().features},
env.app().config().FEES.toFees(),
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().getTimeKeeper().closeTime());
auto txDisable0 = createTx(true, l->seq(), publicKeys[0]);
auto txReEnable1 = createTx(false, l->seq(), publicKeys[1]);
OpenView accum(&*l);
BEAST_EXPECT(applyAndTestResult(env, accum, txDisable0, false));
BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable1, 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().getTimeKeeper().closeTime());
}
BEAST_EXPECT(l->isFlagLedger());
l->updateNegativeUNL();
auto txDisable0 = createTx(true, l->seq(), publicKeys[0]);
auto txDisable1 = createTx(true, l->seq(), publicKeys[1]);
auto txReEnable2 = 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, txDisable0, true));
BEAST_EXPECT(applyAndTestResult(env, accum, txDisable1, false));
BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable2, false));
accum.apply(*l);
auto goodSize = negUnlSizeTest(l, 0, true, false);
BEAST_EXPECT(goodSize);
if (goodSize)
{
BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]);
//++ first ToDisable Tx in ledger's TxSet
uint256 const txID = txDisable0.getTransactionID();
BEAST_EXPECT(l->txExists(txID));
}
}
{
//(3) ledgers before the next flag ledger
for (auto i = 0; i < 256; ++i)
{
auto goodSize = negUnlSizeTest(l, 0, true, false);
BEAST_EXPECT(goodSize);
if (goodSize)
BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]);
l = std::make_shared<Ledger>(*l, env.app().getTimeKeeper().closeTime());
}
BEAST_EXPECT(l->isFlagLedger());
l->updateNegativeUNL();
//(4) next flag ledger
// test if the ledger updated correctly
auto goodSize = negUnlSizeTest(l, 1, false, false);
BEAST_EXPECT(goodSize);
if (goodSize)
{
BEAST_EXPECT(*(l->negativeUNL().begin()) == publicKeys[0]);
nUnlLedgerSeq.emplace(publicKeys[0], l->seq());
}
auto txDisable0 = createTx(true, l->seq(), publicKeys[0]);
auto txDisable1 = createTx(true, l->seq(), publicKeys[1]);
auto txReEnable0 = createTx(false, l->seq(), publicKeys[0]);
auto txReEnable1 = createTx(false, l->seq(), publicKeys[1]);
auto txReEnable2 = createTx(false, l->seq(), publicKeys[2]);
OpenView accum(&*l);
BEAST_EXPECT(applyAndTestResult(env, accum, txDisable0, false));
BEAST_EXPECT(applyAndTestResult(env, accum, txDisable1, true));
BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable1, false));
BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable2, false));
BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable0, true));
accum.apply(*l);
goodSize = negUnlSizeTest(l, 1, true, true);
BEAST_EXPECT(goodSize);
if (goodSize)
{
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 goodSize = negUnlSizeTest(l, 1, true, true);
BEAST_EXPECT(goodSize);
if (goodSize)
{
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().getTimeKeeper().closeTime());
}
BEAST_EXPECT(l->isFlagLedger());
l->updateNegativeUNL();
//(6) next flag ledger
// test if the ledger updated correctly
auto goodSize = negUnlSizeTest(l, 1, false, false);
BEAST_EXPECT(goodSize);
if (goodSize)
{
BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
}
auto txDisable0 = createTx(true, l->seq(), publicKeys[0]);
OpenView accum(&*l);
BEAST_EXPECT(applyAndTestResult(env, accum, txDisable0, true));
accum.apply(*l);
goodSize = negUnlSizeTest(l, 1, true, false);
BEAST_EXPECT(goodSize);
if (goodSize)
{
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 goodSize = negUnlSizeTest(l, 1, true, false);
BEAST_EXPECT(goodSize);
if (goodSize)
{
BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]);
}
l = std::make_shared<Ledger>(*l, env.app().getTimeKeeper().closeTime());
}
BEAST_EXPECT(l->isFlagLedger());
l->updateNegativeUNL();
//(8) next flag ledger
// test if the ledger updated correctly
auto goodSize = negUnlSizeTest(l, 2, false, false);
BEAST_EXPECT(goodSize);
if (goodSize)
{
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 txDisable0 = createTx(true, l->seq(), publicKeys[0]);
auto txReEnable0 = createTx(false, l->seq(), publicKeys[0]);
auto txReEnable1 = createTx(false, l->seq(), publicKeys[1]);
OpenView accum(&*l);
BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable0, true));
BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable1, false));
BEAST_EXPECT(applyAndTestResult(env, accum, txDisable0, false));
accum.apply(*l);
goodSize = negUnlSizeTest(l, 2, false, true);
BEAST_EXPECT(goodSize);
if (goodSize)
{
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 goodSize = negUnlSizeTest(l, 2, false, true);
BEAST_EXPECT(goodSize);
if (goodSize)
{
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().getTimeKeeper().closeTime());
}
BEAST_EXPECT(l->isFlagLedger());
l->updateNegativeUNL();
//(10) next flag ledger
// test if the ledger updated correctly
auto goodSize = negUnlSizeTest(l, 1, false, false);
BEAST_EXPECT(goodSize);
if (goodSize)
{
BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
nUnlLedgerSeq.erase(publicKeys[0]);
BEAST_EXPECT(verifyPubKeyAndSeq(l, nUnlLedgerSeq));
}
auto txReEnable1 = createTx(false, l->seq(), publicKeys[1]);
OpenView accum(&*l);
BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable1, true));
accum.apply(*l);
goodSize = negUnlSizeTest(l, 1, false, true);
BEAST_EXPECT(goodSize);
if (goodSize)
{
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 goodSize = negUnlSizeTest(l, 1, false, true);
BEAST_EXPECT(goodSize);
if (goodSize)
{
BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
BEAST_EXPECT(l->validatorToReEnable() == publicKeys[1]);
}
l = std::make_shared<Ledger>(*l, env.app().getTimeKeeper().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().getTimeKeeper().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::testableAmendments()), 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 kFAKE_AMENDMENT; // So we have different genesis ledgers
auto l = std::make_shared<Ledger>(
kCREATE_GENESIS,
Rules{env.app().config().features},
env.app().config().FEES.toFees(),
std::vector<uint256>{kFAKE_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().getTimeKeeper().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 kEY_PAIR = randomKeyPair(KeyType::Secp256k1);
return std::make_shared<STValidation>(
env.app().getTimeKeeper().now(),
kEY_PAIR.first,
kEY_PAIR.second,
v,
[&](STValidation& v) {
v.setFieldH256(sfLedgerHash, ledger->header().hash);
v.setFieldU32(sfLedgerSequence, ledger->seq());
v.setFlag(kVF_FULL_VALIDATION);
});
};
/**
* 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 const 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);
}
}
}
}
[[nodiscard]] 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 gDefaultPreVote = [](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(gDefaultPreVote)>
bool
voteAndCheck(
NetworkHistory& history,
NodeID const& myId,
std::size_t expect,
PreVote const& pre = gDefaultPreVote)
{
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 const myId(0xA0);
NegativeUNLVote vote(myId, env.journal);
// one add, one remove
auto txSet = std::make_shared<SHAMap>(SHAMapType::TRANSACTION, env.app().getNodeFamily());
PublicKey const toDisableKey(derivePublicKey(KeyType::Ed25519, randomSecretKey()));
PublicKey const toReEnableKey(derivePublicKey(KeyType::Ed25519, randomSecretKey()));
LedgerIndex const seq(1234);
BEAST_EXPECT(countTx(txSet) == 0);
vote.addTx(seq, toDisableKey, NegativeUNLVote::NegativeUNLModify::ToDisable, txSet);
BEAST_EXPECT(countTx(txSet) == 1);
vote.addTx(seq, toReEnableKey, NegativeUNLVote::NegativeUNLModify::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 const env(*this);
NodeID const myId(0xA0);
NegativeUNLVote const vote(myId, env.journal);
uint256 const pad0(0);
uint256 const padF = ~pad0;
NodeID const n1(1);
NodeID const n2(2);
NodeID const n3(3);
std::vector<NodeID> candidates({n1});
BEAST_EXPECT(vote.choose(pad0, candidates) == n1);
BEAST_EXPECT(vote.choose(padF, candidates) == n1);
candidates.emplace_back(2);
BEAST_EXPECT(vote.choose(pad0, candidates) == n1);
BEAST_EXPECT(vote.choose(padF, candidates) == n2);
candidates.emplace_back(3);
BEAST_EXPECT(vote.choose(pad0, candidates) == n1);
BEAST_EXPECT(vote.choose(padF, candidates) == n3);
}
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,
{.numNodes = 10,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = 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,
{.numNodes = 10,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = 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,
{.numNodes = 10,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = 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,
{.numNodes = 10,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = 256 + 2}};
// We need two chains for these tests
bool const 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 const 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 const v1(history.createSTVal(l, myId));
history.validations.add(myId, v1);
RCLValidation const 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,
{.numNodes = 10,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = 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
*/
static 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 const rightDisable = disableCandidates.size() == numDisable;
bool const 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,
{.numNodes = 35,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = 0}};
hash_set<NodeID> negUnl012;
for (std::uint32_t i = 0; i < 3; ++i)
negUnl012.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::kNEGATIVE_UNL_HIGH_WATER_MARK + 1;
NegativeUNLVote vote(history.UNLNodeIDs[0], history.env.journal);
{
// all good scores
BEAST_EXPECT(
checkCandidateSizes(vote, history.UNLNodeIDSet, negUnl012, goodScoreTable, 0, 3));
}
{
// all bad scores
hash_map<NodeID, std::uint32_t> scoreTable;
for (auto& n : history.UNLNodeIDs)
scoreTable[n] = NegativeUNLVote::kNEGATIVE_UNL_LOW_WATER_MARK - 1;
BEAST_EXPECT(
checkCandidateSizes(vote, history.UNLNodeIDSet, negUnl012, scoreTable, 35 - 3, 0));
}
{
// all between watermarks
hash_map<NodeID, std::uint32_t> scoreTable;
for (auto& n : history.UNLNodeIDs)
scoreTable[n] = NegativeUNLVote::kNEGATIVE_UNL_LOW_WATER_MARK + 1;
BEAST_EXPECT(
checkCandidateSizes(vote, history.UNLNodeIDSet, negUnl012, scoreTable, 0, 0));
}
{
// 2 good scorers in negUnl
auto scoreTable = goodScoreTable;
scoreTable[*negUnl012.begin()] = NegativeUNLVote::kNEGATIVE_UNL_LOW_WATER_MARK + 1;
BEAST_EXPECT(
checkCandidateSizes(vote, history.UNLNodeIDSet, negUnl012, scoreTable, 0, 2));
}
{
// 2 bad scorers not in negUnl
auto scoreTable = goodScoreTable;
scoreTable[history.UNLNodeIDs[11]] = NegativeUNLVote::kNEGATIVE_UNL_LOW_WATER_MARK - 1;
scoreTable[history.UNLNodeIDs[12]] = NegativeUNLVote::kNEGATIVE_UNL_LOW_WATER_MARK - 1;
BEAST_EXPECT(
checkCandidateSizes(vote, history.UNLNodeIDSet, negUnl012, scoreTable, 2, 3));
}
{
// 2 in negUnl but not in unl, have a remove candidate from score
// table
hash_set<NodeID> unlTemp = history.UNLNodeIDSet;
unlTemp.erase(history.UNLNodeIDs[0]);
unlTemp.erase(history.UNLNodeIDs[1]);
BEAST_EXPECT(checkCandidateSizes(vote, unlTemp, negUnl012, 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::kNEGATIVE_UNL_LOW_WATER_MARK + 1;
hash_set<NodeID> unlTemp = history.UNLNodeIDSet;
unlTemp.erase(history.UNLNodeIDs[0]);
unlTemp.erase(history.UNLNodeIDs[1]);
BEAST_EXPECT(checkCandidateSizes(vote, unlTemp, negUnl012, scoreTable, 0, 2));
}
{
// 2 new validators
NodeID const new1(0xbead);
NodeID const new2(0xbeef);
hash_set<NodeID> const nowTrusted = {new1, new2};
hash_set<NodeID> unlTemp = history.UNLNodeIDSet;
unlTemp.insert(new1);
unlTemp.insert(new2);
vote.newValidators(256, nowTrusted);
{
// 2 new validators have good scores, already in negUnl
auto scoreTable = goodScoreTable;
scoreTable[new1] = NegativeUNLVote::kNEGATIVE_UNL_HIGH_WATER_MARK + 1;
scoreTable[new2] = NegativeUNLVote::kNEGATIVE_UNL_HIGH_WATER_MARK + 1;
hash_set<NodeID> negUnlTemp = negUnl012;
negUnlTemp.insert(new1);
negUnlTemp.insert(new2);
BEAST_EXPECT(checkCandidateSizes(vote, unlTemp, negUnlTemp, scoreTable, 0, 3 + 2));
}
{
// 2 new validators have bad scores, not in negUnl
auto scoreTable = goodScoreTable;
scoreTable[new1] = 0;
scoreTable[new2] = 0;
BEAST_EXPECT(checkCandidateSizes(vote, unlTemp, negUnl012, scoreTable, 0, 3));
}
{
// expired the new validators have bad scores, not in negUnl
vote.purgeNewValidators(256 + NegativeUNLVote::kNEW_VALIDATOR_DISABLE_SKIP + 1);
auto scoreTable = goodScoreTable;
scoreTable[new1] = 0;
scoreTable[new2] = 0;
BEAST_EXPECT(checkCandidateSizes(vote, unlTemp, negUnl012, 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 const env(*this);
NodeID const myId(0xA0);
NegativeUNLVote vote(myId, env.journal);
std::array<std::uint32_t, 3> const unlSizes = {34, 35, 80};
std::array<std::uint32_t, 3> const nUnlPercent = {0, 50, 100};
std::array<std::uint32_t, 8> scores = {
0,
NegativeUNLVote::kNEGATIVE_UNL_LOW_WATER_MARK - 1,
NegativeUNLVote::kNEGATIVE_UNL_LOW_WATER_MARK,
NegativeUNLVote::kNEGATIVE_UNL_LOW_WATER_MARK + 1,
NegativeUNLVote::kNEGATIVE_UNL_HIGH_WATER_MARK - 1,
NegativeUNLVote::kNEGATIVE_UNL_HIGH_WATER_MARK,
NegativeUNLVote::kNEGATIVE_UNL_HIGH_WATER_MARK + 1,
NegativeUNLVote::kNEGATIVE_UNL_MIN_LOCAL_VALS_TO_VOTE};
//== combination 1:
{
auto fillScoreTable = [&](std::uint32_t unlSize,
std::uint32_t nUnlSize,
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> const keys = createPublicKeys(unlSize);
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 < nUnlSize; ++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 toDisableExpect = 0;
std::size_t toReEnableExpect = 0;
if (np == 0)
{
if (score < NegativeUNLVote::kNEGATIVE_UNL_LOW_WATER_MARK)
{
toDisableExpect = us;
}
}
else if (np == 50)
{
if (score > NegativeUNLVote::kNEGATIVE_UNL_HIGH_WATER_MARK)
{
toReEnableExpect = us * np / 100;
}
}
else
{
if (score > NegativeUNLVote::kNEGATIVE_UNL_HIGH_WATER_MARK)
{
toReEnableExpect = us;
}
}
BEAST_EXPECT(checkCandidateSizes(
vote, unl, negUnl, scoreTable, toDisableExpect, toReEnableExpect));
}
}
}
//== combination 2:
{
auto fillScoreTable = [&](std::uint32_t unlSize,
std::uint32_t nUnlPercent,
hash_set<NodeID>& unl,
hash_set<NodeID>& negUnl,
hash_map<NodeID, std::uint32_t>& scoreTable) {
std::vector<NodeID> nodeIDs;
std::vector<PublicKey> const keys = createPublicKeys(unlSize);
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 < unlSize;)
{
scoreTable[nodeIDs[nIdx++]] = scores.back();
}
if (nUnlPercent == 100)
{
negUnl = unl;
}
else if (nUnlPercent == 50)
{
for (std::uint32_t i = 1; i < unlSize; 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 toDisableExpect = 0;
std::size_t toReEnableExpect = 0;
if (np == 0)
{
toDisableExpect = 4;
}
else if (np == 50)
{
toReEnableExpect = negUnl.size() - 6;
}
else
{
toReEnableExpect = negUnl.size() - 12;
}
BEAST_EXPECT(checkCandidateSizes(
vote, unl, negUnl, scoreTable, toDisableExpect, toReEnableExpect));
}
}
}
}
}
void
testNewValidators()
{
testcase("New Validators");
jtx::Env const env(*this);
NodeID const 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 const n1(0xA1);
NodeID const n2(0xA2);
NodeID const 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::kNEW_VALIDATOR_DISABLE_SKIP, {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::kNEW_VALIDATOR_DISABLE_SKIP);
}
vote.purgeNewValidators(NegativeUNLVote::kNEW_VALIDATOR_DISABLE_SKIP + 2);
BEAST_EXPECT(vote.newValidators_.size() == 3);
vote.purgeNewValidators(NegativeUNLVote::kNEW_VALIDATOR_DISABLE_SKIP + 3);
BEAST_EXPECT(vote.newValidators_.size() == 2);
vote.purgeNewValidators(NegativeUNLVote::kNEW_VALIDATOR_DISABLE_SKIP + 4);
BEAST_EXPECT(vote.newValidators_.size() == 1);
BEAST_EXPECT(vote.newValidators_.begin()->first == n3);
BEAST_EXPECT(
vote.newValidators_.begin()->second == NegativeUNLVote::kNEW_VALIDATOR_DISABLE_SKIP);
}
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> const 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,
{.numNodes = unlSize,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = 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 const add50 = scorePattern[sp][k] == 50 && l->seq() % 2 == 0;
bool const add100 = scorePattern[sp][k] == 100;
bool const addMe = history.UNLNodeIDs[idx] == myId;
return add50 || add100 || addMe;
});
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;
}
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,
{.numNodes = 51,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = {}}};
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,
{.numNodes = 37,
.negUNLSize = 0,
.hasToDisable = true,
.hasToReEnable = false,
.numLedgers = {}}};
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,
{.numNodes = 29,
.negUNLSize = 1,
.hasToDisable = false,
.hasToReEnable = true,
.numLedgers = {}}};
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,
{.numNodes = 30,
.negUNLSize = 1,
.hasToDisable = true,
.hasToReEnable = false,
.numLedgers = {}}};
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,
{.numNodes = 32,
.negUNLSize = 8,
.hasToDisable = true,
.hasToReEnable = true,
.numLedgers = {}}};
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,
{.numNodes = 35,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = {}}};
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,
{.numNodes = 40,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = {}}};
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,
{.numNodes = 25,
.negUNLSize = 2,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = {}}};
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,
{.numNodes = 15,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = {}}};
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 extraKey1 = randomKeyPair(KeyType::Ed25519).first;
auto extraKey2 = randomKeyPair(KeyType::Ed25519).first;
history.UNLKeySet.insert(extraKey1);
history.UNLKeySet.insert(extraKey2);
hash_set<NodeID> nowTrusted;
nowTrusted.insert(calcNodeID(extraKey1));
nowTrusted.insert(calcNodeID(extraKey2));
vote.newValidators(history.lastLedger()->seq(), nowTrusted);
}));
}
}
{
//== 2 expired new validators have bad scores
//-- txSet.size = 1
NetworkHistory history = {
*this,
{.numNodes = 21,
.negUNLSize = 0,
.hasToDisable = false,
.hasToReEnable = false,
.numLedgers = NegativeUNLVote::kNEW_VALIDATOR_DISABLE_SKIP * 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 extraKey1 = randomKeyPair(KeyType::Ed25519).first;
auto extraKey2 = randomKeyPair(KeyType::Ed25519).first;
history.UNLKeySet.insert(extraKey1);
history.UNLKeySet.insert(extraKey2);
hash_set<NodeID> nowTrusted;
nowTrusted.insert(calcNodeID(extraKey1));
nowTrusted.insert(calcNodeID(extraKey2));
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>(
kCREATE_GENESIS,
Rules{env.app().config().features},
env.app().config().FEES.toFees(),
std::vector<uint256>{},
env.app().getNodeFamily());
auto createSTVal = [&](std::pair<PublicKey, SecretKey> const& keys) {
return std::make_shared<STValidation>(
env.app().getTimeKeeper().now(),
keys.first,
keys.second,
calcNodeID(keys.first),
[&](STValidation& v) {
v.setFieldH256(sfLedgerHash, l->header().hash);
v.setFieldU32(sfLedgerSequence, l->seq());
v.setFlag(kVF_FULL_VALIDATION);
});
};
// create keys and validations
std::uint32_t const numNodes = 10;
std::uint32_t const 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().getValidators();
auto& local = *nUnlKeys.begin();
std::vector<std::string> const cfgPublishers;
validators.load(local, cfgKeys, cfgPublishers);
validators.updateTrusted(
activeValidators,
env.timeKeeper().now(),
env.app().getOPs(),
env.app().getOverlay(),
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 const sameSize = l->negativeUNL().size() == size;
bool const sameToDisable = (l->validatorToDisable() != std::nullopt) == hasToDisable;
bool const 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);
}
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 const pk(s);
auto it = nUnlLedgerSeq.find(pk);
if (it == nUnlLedgerSeq.end())
return false;
if (it->second != seq)
return false;
nUnlLedgerSeq.erase(it);
}
return nUnlLedgerSeq.empty();
}
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 const ss = 33;
std::vector<uint8_t> data(ss, 0);
data[0] = 0xED;
for (int i = 0; i < n; ++i)
{
data[1]++;
Slice const 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 xrpl::test