Page MenuHomePhabricator

No OneTemporary

diff --git a/src/avalanche/test/processor_tests.cpp b/src/avalanche/test/processor_tests.cpp
index 7f56a431c..7285ceb11 100644
--- a/src/avalanche/test/processor_tests.cpp
+++ b/src/avalanche/test/processor_tests.cpp
@@ -1,1917 +1,1913 @@
// Copyright (c) 2018-2020 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <avalanche/processor.h>
#include <arith_uint256.h>
#include <avalanche/avalanche.h>
#include <avalanche/delegationbuilder.h>
#include <avalanche/peermanager.h>
#include <avalanche/proofbuilder.h>
#include <avalanche/voterecord.h>
#include <chain.h>
#include <config.h>
#include <key_io.h>
#include <net_processing.h> // For ::PeerManager
#include <reverse_iterator.h>
#include <scheduler.h>
#include <util/time.h>
#include <util/translation.h> // For bilingual_str
// D6970 moved LookupBlockIndex from chain.h to validation.h TODO: remove this
// when LookupBlockIndex is refactored out of validation
#include <validation.h>
#include <avalanche/test/util.h>
#include <test/util/setup_common.h>
#include <boost/mpl/list.hpp>
#include <boost/test/unit_test.hpp>
#include <functional>
#include <type_traits>
#include <vector>
using namespace avalanche;
namespace avalanche {
namespace {
struct AvalancheTest {
static void runEventLoop(avalanche::Processor &p) { p.runEventLoop(); }
static std::vector<CInv> getInvsForNextPoll(Processor &p) {
return p.getInvsForNextPoll(false);
}
static NodeId getSuitableNodeToQuery(Processor &p) {
return WITH_LOCK(p.cs_peerManager,
return p.peerManager->selectNode());
}
static uint64_t getRound(const Processor &p) { return p.round; }
static uint32_t getMinQuorumScore(const Processor &p) {
return p.minQuorumScore;
}
static double getMinQuorumConnectedScoreRatio(const Processor &p) {
return p.minQuorumConnectedScoreRatio;
}
static void clearavaproofsNodeCounter(Processor &p) {
p.avaproofsNodeCounter = 0;
}
};
} // namespace
} // namespace avalanche
namespace {
struct CConnmanTest : public CConnman {
using CConnman::CConnman;
void AddNode(CNode &node) {
LOCK(cs_vNodes);
vNodes.push_back(&node);
}
void ClearNodes() {
LOCK(cs_vNodes);
for (CNode *node : vNodes) {
delete node;
}
vNodes.clear();
}
};
CService ip(uint32_t i) {
struct in_addr s;
s.s_addr = i;
return CService(CNetAddr(s), Params().GetDefaultPort());
}
struct AvalancheTestingSetup : public TestChain100Setup {
const ::Config &config;
CConnmanTest *m_connman;
std::unique_ptr<Processor> m_processor;
// The master private key we delegate to.
CKey masterpriv;
std::unordered_set<std::string> m_overridden_args;
AvalancheTestingSetup()
: TestChain100Setup(), config(GetConfig()),
masterpriv(CKey::MakeCompressedKey()) {
// Deterministic randomness for tests.
auto connman = std::make_unique<CConnmanTest>(config, 0x1337, 0x1337,
*m_node.addrman);
m_connman = connman.get();
m_node.connman = std::move(connman);
m_node.peerman = ::PeerManager::make(
config.GetChainParams(), *m_connman, *m_node.addrman,
m_node.banman.get(), *m_node.chainman, *m_node.mempool, false);
m_node.chain = interfaces::MakeChain(m_node, config.GetChainParams());
// Get the processor ready.
setArg("-avaminquorumstake", "0");
setArg("-avaminquorumconnectedstakeratio", "0");
setArg("avaminavaproofsnodecount", "0");
setArg("-avaproofstakeutxoconfirmations", "1");
bilingual_str error;
m_processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(),
*Assert(m_node.chainman), *m_node.scheduler, error);
BOOST_CHECK(m_processor);
}
~AvalancheTestingSetup() {
m_connman->ClearNodes();
SyncWithValidationInterfaceQueue();
ArgsManager &argsman = *Assert(m_node.args);
for (const std::string &key : m_overridden_args) {
argsman.ClearForcedArg(key);
}
m_overridden_args.clear();
}
CNode *ConnectNode(ServiceFlags nServices) {
static NodeId id = 0;
CAddress addr(ip(GetRandInt(0xffffffff)), NODE_NONE);
auto node =
new CNode(id++, ServiceFlags(NODE_NETWORK), INVALID_SOCKET, addr,
/* nKeyedNetGroupIn */ 0,
/* nLocalHostNonceIn */ 0,
/* nLocalExtraEntropyIn */ 0, CAddress(),
/* pszDest */ "", ConnectionType::OUTBOUND_FULL_RELAY,
/* inbound_onion */ false);
node->SetCommonVersion(PROTOCOL_VERSION);
node->nServices = nServices;
m_node.peerman->InitializeNode(config, node);
node->nVersion = 1;
node->fSuccessfullyConnected = true;
m_connman->AddNode(*node);
return node;
}
ProofRef GetProof() {
const CKey key = CKey::MakeCompressedKey();
const COutPoint outpoint{TxId(GetRandHash()), 0};
CScript script = GetScriptForDestination(PKHash(key.GetPubKey()));
const Amount amount = PROOF_DUST_THRESHOLD;
const uint32_t height = 100;
LOCK(cs_main);
CCoinsViewCache &coins =
Assert(m_node.chainman)->ActiveChainstate().CoinsTip();
coins.AddCoin(outpoint, Coin(CTxOut(amount, script), height, false),
false);
ProofBuilder pb(0, 0, masterpriv, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
BOOST_CHECK(pb.addUTXO(outpoint, amount, height, false, key));
return pb.build();
}
bool addNode(NodeId nodeid, const ProofId &proofid) {
return m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
return pm.addNode(nodeid, proofid);
});
}
bool addNode(NodeId nodeid) {
auto proof = GetProof();
return m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
return pm.registerProof(proof) &&
pm.addNode(nodeid, proof->getId());
});
}
std::array<CNode *, 8> ConnectNodes() {
auto proof = GetProof();
BOOST_CHECK(
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
return pm.registerProof(proof);
}));
const ProofId &proofid = proof->getId();
std::array<CNode *, 8> nodes;
for (CNode *&n : nodes) {
n = ConnectNode(NODE_AVALANCHE);
BOOST_CHECK(addNode(n->GetId(), proofid));
}
return nodes;
}
void runEventLoop() { AvalancheTest::runEventLoop(*m_processor); }
NodeId getSuitableNodeToQuery() {
return AvalancheTest::getSuitableNodeToQuery(*m_processor);
}
std::vector<CInv> getInvsForNextPoll() {
return AvalancheTest::getInvsForNextPoll(*m_processor);
}
uint64_t getRound() const { return AvalancheTest::getRound(*m_processor); }
bool registerVotes(NodeId nodeid, const avalanche::Response &response,
std::vector<avalanche::VoteItemUpdate> &updates,
std::string &error) {
int banscore;
return m_processor->registerVotes(nodeid, response, updates, banscore,
error);
}
bool registerVotes(NodeId nodeid, const avalanche::Response &response,
std::vector<avalanche::VoteItemUpdate> &updates) {
int banscore;
std::string error;
return m_processor->registerVotes(nodeid, response, updates, banscore,
error);
}
void setArg(std::string key, std::string value) {
ArgsManager &argsman = *Assert(m_node.args);
argsman.ForceSetArg(key, std::move(value));
m_overridden_args.emplace(std::move(key));
}
+
+ bool addToReconcile(const AnyVoteItem &item) {
+ return m_processor->addToReconcile(item);
+ }
};
struct BlockProvider {
AvalancheTestingSetup *fixture;
uint32_t invType;
BlockProvider(AvalancheTestingSetup *_fixture)
: fixture(_fixture), invType(MSG_BLOCK) {}
CBlockIndex *buildVoteItem() const {
CBlock block = fixture->CreateAndProcessBlock({}, CScript());
const BlockHash blockHash = block.GetHash();
LOCK(cs_main);
return Assert(fixture->m_node.chainman)
->m_blockman.LookupBlockIndex(blockHash);
}
uint256 getVoteItemId(const CBlockIndex *pindex) const {
return pindex->GetBlockHash();
}
- bool addToReconcile(const CBlockIndex *pindex) {
- return fixture->m_processor->addToReconcile(pindex);
- }
-
std::vector<Vote> buildVotesForItems(uint32_t error,
std::vector<CBlockIndex *> &&items) {
size_t numItems = items.size();
std::vector<Vote> votes;
votes.reserve(numItems);
// Votes are sorted by most work first
std::sort(items.begin(), items.end(), CBlockIndexWorkComparator());
for (auto &item : reverse_iterate(items)) {
votes.emplace_back(error, item->GetBlockHash());
}
return votes;
}
void invalidateItem(CBlockIndex *pindex) {
LOCK(::cs_main);
pindex->nStatus = pindex->nStatus.withFailed();
}
const CBlockIndex *fromAnyVoteItem(const AnyVoteItem &item) {
return std::get<const CBlockIndex *>(item);
}
};
struct ProofProvider {
AvalancheTestingSetup *fixture;
uint32_t invType;
ProofProvider(AvalancheTestingSetup *_fixture)
: fixture(_fixture), invType(MSG_AVA_PROOF) {}
ProofRef buildVoteItem() const {
const ProofRef proof = fixture->GetProof();
fixture->m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof));
});
return proof;
}
uint256 getVoteItemId(const ProofRef &proof) const {
return proof->getId();
}
- bool addToReconcile(const ProofRef &proof) {
- return fixture->m_processor->addToReconcile(proof);
- }
-
std::vector<Vote> buildVotesForItems(uint32_t error,
std::vector<ProofRef> &&items) {
size_t numItems = items.size();
std::vector<Vote> votes;
votes.reserve(numItems);
// Votes are sorted by high score first
std::sort(items.begin(), items.end(), ProofComparatorByScore());
for (auto &item : items) {
votes.emplace_back(error, item->getId());
}
return votes;
}
void invalidateItem(const ProofRef &proof) {
fixture->m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.rejectProof(proof->getId(),
avalanche::PeerManager::RejectionMode::INVALIDATE);
});
}
const ProofRef fromAnyVoteItem(const AnyVoteItem &item) {
return std::get<const ProofRef>(item);
}
};
} // namespace
BOOST_FIXTURE_TEST_SUITE(processor_tests, AvalancheTestingSetup)
// FIXME A std::tuple can be used instead of boost::mpl::list after boost 1.67
using VoteItemProviders = boost::mpl::list<BlockProvider, ProofProvider>;
BOOST_AUTO_TEST_CASE_TEMPLATE(voteitemupdate, P, VoteItemProviders) {
P provider(this);
std::set<VoteStatus> status{
VoteStatus::Invalid, VoteStatus::Rejected, VoteStatus::Accepted,
VoteStatus::Finalized, VoteStatus::Stale,
};
auto item = provider.buildVoteItem();
for (auto s : status) {
VoteItemUpdate itemUpdate(item, s);
// The use of BOOST_CHECK instead of BOOST_CHECK_EQUAL prevents from
// having to define operator<<() for each argument type.
BOOST_CHECK(provider.fromAnyVoteItem(itemUpdate.getVoteItem()) == item);
BOOST_CHECK(itemUpdate.getStatus() == s);
}
}
BOOST_AUTO_TEST_CASE_TEMPLATE(item_reconcile_twice, P, VoteItemProviders) {
P provider(this);
auto item = provider.buildVoteItem();
// Adding the item twice does nothing.
- BOOST_CHECK(provider.addToReconcile(item));
- BOOST_CHECK(!provider.addToReconcile(item));
+ BOOST_CHECK(addToReconcile(item));
+ BOOST_CHECK(!addToReconcile(item));
BOOST_CHECK(m_processor->isAccepted(item));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(item_null, P, VoteItemProviders) {
P provider(this);
// Check that null case is handled on the public interface
BOOST_CHECK(!m_processor->isAccepted(nullptr));
BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1);
auto item = decltype(provider.buildVoteItem())();
BOOST_CHECK(item == nullptr);
- BOOST_CHECK(!provider.addToReconcile(item));
+ BOOST_CHECK(!addToReconcile(item));
// Check that adding item to vote on doesn't change the outcome. A
// comparator is used under the hood, and this is skipped if there are no
// vote records.
item = provider.buildVoteItem();
- BOOST_CHECK(provider.addToReconcile(item));
+ BOOST_CHECK(addToReconcile(item));
BOOST_CHECK(!m_processor->isAccepted(nullptr));
BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1);
}
namespace {
Response next(Response &r) {
auto copy = r;
r = {r.getRound() + 1, r.getCooldown(), r.GetVotes()};
return copy;
}
} // namespace
BOOST_AUTO_TEST_CASE_TEMPLATE(vote_item_register, P, VoteItemProviders) {
P provider(this);
const uint32_t invType = provider.invType;
auto item = provider.buildVoteItem();
auto itemid = provider.getVoteItemId(item);
// Create nodes that supports avalanche.
auto avanodes = ConnectNodes();
// Querying for random item returns false.
BOOST_CHECK(!m_processor->isAccepted(item));
// Add a new item. Check it is added to the polls.
- BOOST_CHECK(provider.addToReconcile(item));
+ BOOST_CHECK(addToReconcile(item));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
BOOST_CHECK(m_processor->isAccepted(item));
int nextNodeIndex = 0;
std::vector<avalanche::VoteItemUpdate> updates;
auto registerNewVote = [&](const Response &resp) {
runEventLoop();
auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
BOOST_CHECK(registerVotes(nodeid, resp, updates));
};
// Let's vote for this item a few times.
Response resp{0, 0, {Vote(0, itemid)}};
for (int i = 0; i < 6; i++) {
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 0);
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// A single neutral vote do not change anything.
resp = {getRound(), 0, {Vote(-1, itemid)}};
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 0);
BOOST_CHECK_EQUAL(updates.size(), 0);
resp = {getRound(), 0, {Vote(0, itemid)}};
for (int i = 1; i < 7; i++) {
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), i);
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// Two neutral votes will stall progress.
resp = {getRound(), 0, {Vote(-1, itemid)}};
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 6);
BOOST_CHECK_EQUAL(updates.size(), 0);
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 6);
BOOST_CHECK_EQUAL(updates.size(), 0);
resp = {getRound(), 0, {Vote(0, itemid)}};
for (int i = 2; i < 8; i++) {
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 6);
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// We vote for it numerous times to finalize it.
for (int i = 7; i < AVALANCHE_FINALIZATION_SCORE; i++) {
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), i);
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// As long as it is not finalized, we poll.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
// Now finalize the decision.
registerNewVote(next(resp));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
updates.clear();
// Once the decision is finalized, there is no poll for it.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
// Get a new item to vote on
item = provider.buildVoteItem();
itemid = provider.getVoteItemId(item);
- BOOST_CHECK(provider.addToReconcile(item));
+ BOOST_CHECK(addToReconcile(item));
// Now let's finalize rejection.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
resp = {getRound(), 0, {Vote(1, itemid)}};
for (int i = 0; i < 6; i++) {
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// Now the state will flip.
registerNewVote(next(resp));
BOOST_CHECK(!m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Rejected);
updates.clear();
// Now it is rejected, but we can vote for it numerous times.
for (int i = 1; i < AVALANCHE_FINALIZATION_SCORE; i++) {
registerNewVote(next(resp));
BOOST_CHECK(!m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// As long as it is not finalized, we poll.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
// Now finalize the decision.
registerNewVote(next(resp));
BOOST_CHECK(!m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Invalid);
updates.clear();
// Once the decision is finalized, there is no poll for it.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
}
BOOST_AUTO_TEST_CASE_TEMPLATE(multi_item_register, P, VoteItemProviders) {
P provider(this);
const uint32_t invType = provider.invType;
auto itemA = provider.buildVoteItem();
auto itemidA = provider.getVoteItemId(itemA);
auto itemB = provider.buildVoteItem();
auto itemidB = provider.getVoteItemId(itemB);
// Create several nodes that support avalanche.
auto avanodes = ConnectNodes();
// Querying for random item returns false.
BOOST_CHECK(!m_processor->isAccepted(itemA));
BOOST_CHECK(!m_processor->isAccepted(itemB));
// Start voting on item A.
- BOOST_CHECK(provider.addToReconcile(itemA));
+ BOOST_CHECK(addToReconcile(itemA));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemidA);
uint64_t round = getRound();
runEventLoop();
std::vector<avalanche::VoteItemUpdate> updates;
BOOST_CHECK(registerVotes(avanodes[0]->GetId(),
{round, 0, {Vote(0, itemidA)}}, updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
// Start voting on item B after one vote.
std::vector<Vote> votes = provider.buildVotesForItems(0, {itemA, itemB});
Response resp{round + 1, 0, votes};
- BOOST_CHECK(provider.addToReconcile(itemB));
+ BOOST_CHECK(addToReconcile(itemB));
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 2);
// Ensure the inv ordering is as expected
for (size_t i = 0; i < invs.size(); i++) {
BOOST_CHECK_EQUAL(invs[i].type, invType);
BOOST_CHECK(invs[i].hash == votes[i].GetHash());
}
// Let's vote for these items a few times.
for (int i = 0; i < 4; i++) {
NodeId nodeid = getSuitableNodeToQuery();
runEventLoop();
BOOST_CHECK(registerVotes(nodeid, next(resp), updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// Now it is accepted, but we can vote for it numerous times.
for (int i = 0; i < AVALANCHE_FINALIZATION_SCORE; i++) {
NodeId nodeid = getSuitableNodeToQuery();
runEventLoop();
BOOST_CHECK(registerVotes(nodeid, next(resp), updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// Running two iterration of the event loop so that vote gets triggered on A
// and B.
NodeId firstNodeid = getSuitableNodeToQuery();
runEventLoop();
NodeId secondNodeid = getSuitableNodeToQuery();
runEventLoop();
BOOST_CHECK(firstNodeid != secondNodeid);
// Next vote will finalize item A.
BOOST_CHECK(registerVotes(firstNodeid, next(resp), updates));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == itemA);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
updates.clear();
// We do not vote on A anymore.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemidB);
// Next vote will finalize item B.
BOOST_CHECK(registerVotes(secondNodeid, resp, updates));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == itemB);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
updates.clear();
// There is nothing left to vote on.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
}
BOOST_AUTO_TEST_CASE_TEMPLATE(poll_and_response, P, VoteItemProviders) {
P provider(this);
const uint32_t invType = provider.invType;
auto item = provider.buildVoteItem();
auto itemid = provider.getVoteItemId(item);
// There is no node to query.
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE);
// Add enough nodes to have a valid quorum, and the same amount with no
// avalanche support
std::set<NodeId> avanodeIds;
auto avanodes = ConnectNodes();
for (auto avanode : avanodes) {
ConnectNode(NODE_NONE);
avanodeIds.insert(avanode->GetId());
}
auto getSelectedAvanodeId = [&]() {
NodeId avanodeid = getSuitableNodeToQuery();
BOOST_CHECK(avanodeIds.find(avanodeid) != avanodeIds.end());
return avanodeid;
};
// It returns one of the avalanche peer.
NodeId avanodeid = getSelectedAvanodeId();
// Register an item and check it is added to the list of elements to poll.
- BOOST_CHECK(provider.addToReconcile(item));
+ BOOST_CHECK(addToReconcile(item));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
std::set<NodeId> unselectedNodeids = avanodeIds;
unselectedNodeids.erase(avanodeid);
const size_t remainingNodeIds = unselectedNodeids.size();
uint64_t round = getRound();
for (size_t i = 0; i < remainingNodeIds; i++) {
// Trigger a poll on avanode.
runEventLoop();
// Another node is selected
NodeId nodeid = getSuitableNodeToQuery();
BOOST_CHECK(unselectedNodeids.find(nodeid) != avanodeIds.end());
unselectedNodeids.erase(nodeid);
}
// There is no more suitable peer available, so return nothing.
BOOST_CHECK(unselectedNodeids.empty());
runEventLoop();
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE);
// Respond to the request.
Response resp = {round, 0, {Vote(0, itemid)}};
std::vector<avalanche::VoteItemUpdate> updates;
BOOST_CHECK(registerVotes(avanodeid, resp, updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
// Now that avanode fullfilled his request, it is added back to the list of
// queriable nodes.
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
auto checkRegisterVotesError = [&](NodeId nodeid,
const avalanche::Response &response,
const std::string &expectedError) {
std::string error;
BOOST_CHECK(!registerVotes(nodeid, response, updates, error));
BOOST_CHECK_EQUAL(error, expectedError);
BOOST_CHECK_EQUAL(updates.size(), 0);
};
// Sending a response when not polled fails.
checkRegisterVotesError(avanodeid, next(resp), "unexpected-ava-response");
// Trigger a poll on avanode.
round = getRound();
runEventLoop();
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE);
// Sending responses that do not match the request also fails.
// 1. Too many results.
resp = {round, 0, {Vote(0, itemid), Vote(0, itemid)}};
runEventLoop();
checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-size");
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
// 2. Not enough results.
resp = {getRound(), 0, {}};
runEventLoop();
checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-size");
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
// 3. Do not match the poll.
resp = {getRound(), 0, {Vote()}};
runEventLoop();
checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-content");
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
// At this stage we have reached the max inflight requests for our inv, so
// it won't be requested anymore until the requests are fullfilled. Let's
// vote on another item with no inflight request so the remaining tests
// makes sense.
invs = getInvsForNextPoll();
BOOST_CHECK(invs.empty());
item = provider.buildVoteItem();
itemid = provider.getVoteItemId(item);
- BOOST_CHECK(provider.addToReconcile(item));
+ BOOST_CHECK(addToReconcile(item));
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
// 4. Invalid round count. Request is not discarded.
uint64_t queryRound = getRound();
runEventLoop();
resp = {queryRound + 1, 0, {Vote()}};
checkRegisterVotesError(avanodeid, resp, "unexpected-ava-response");
resp = {queryRound - 1, 0, {Vote()}};
checkRegisterVotesError(avanodeid, resp, "unexpected-ava-response");
// 5. Making request for invalid nodes do not work. Request is not
// discarded.
resp = {queryRound, 0, {Vote(0, itemid)}};
checkRegisterVotesError(avanodeid + 1234, resp, "unexpected-ava-response");
// Proper response gets processed and avanode is available again.
resp = {queryRound, 0, {Vote(0, itemid)}};
BOOST_CHECK(registerVotes(avanodeid, resp, updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
// Out of order response are rejected.
const auto item2 = provider.buildVoteItem();
- BOOST_CHECK(provider.addToReconcile(item2));
+ BOOST_CHECK(addToReconcile(item2));
std::vector<Vote> votes = provider.buildVotesForItems(0, {item, item2});
resp = {getRound(), 0, {votes[1], votes[0]}};
runEventLoop();
checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-content");
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
// But they are accepted in order.
resp = {getRound(), 0, votes};
runEventLoop();
BOOST_CHECK(registerVotes(avanodeid, resp, updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
}
BOOST_AUTO_TEST_CASE_TEMPLATE(dont_poll_invalid_item, P, VoteItemProviders) {
P provider(this);
const uint32_t invType = provider.invType;
auto itemA = provider.buildVoteItem();
auto itemB = provider.buildVoteItem();
auto avanodes = ConnectNodes();
// Build votes to get proper ordering
std::vector<Vote> votes = provider.buildVotesForItems(0, {itemA, itemB});
// Register the items and check they are added to the list of elements to
// poll.
- BOOST_CHECK(provider.addToReconcile(itemA));
- BOOST_CHECK(provider.addToReconcile(itemB));
+ BOOST_CHECK(addToReconcile(itemA));
+ BOOST_CHECK(addToReconcile(itemB));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 2);
for (size_t i = 0; i < invs.size(); i++) {
BOOST_CHECK_EQUAL(invs[i].type, invType);
BOOST_CHECK(invs[i].hash == votes[i].GetHash());
}
// When an item is marked invalid, stop polling.
provider.invalidateItem(itemB);
Response goodResp{getRound(), 0, {Vote(0, provider.getVoteItemId(itemA))}};
std::vector<avalanche::VoteItemUpdate> updates;
runEventLoop();
BOOST_CHECK(registerVotes(avanodes[0]->GetId(), goodResp, updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
// Votes including itemB are rejected
Response badResp{getRound(), 0, votes};
runEventLoop();
std::string error;
BOOST_CHECK(!registerVotes(avanodes[1]->GetId(), badResp, updates, error));
BOOST_CHECK_EQUAL(error, "invalid-ava-response-size");
}
BOOST_TEST_DECORATOR(*boost::unit_test::timeout(60))
BOOST_AUTO_TEST_CASE_TEMPLATE(poll_inflight_timeout, P, VoteItemProviders) {
P provider(this);
ChainstateManager &chainman = *Assert(m_node.chainman);
auto queryTimeDuration = std::chrono::milliseconds(10);
setArg("-avatimeout", ToString(queryTimeDuration.count()));
setArg("-avaminquorumstake", "0");
setArg("-avaminquorumconnectedstakeratio", "0");
setArg("avaminavaproofsnodecount", "0");
bilingual_str error;
m_processor = Processor::MakeProcessor(*m_node.args, *m_node.chain,
m_node.connman.get(), chainman,
*m_node.scheduler, error);
const auto item = provider.buildVoteItem();
const auto itemid = provider.getVoteItemId(item);
// Add the item
- BOOST_CHECK(provider.addToReconcile(item));
+ BOOST_CHECK(addToReconcile(item));
// Create a quorum of nodes that support avalanche.
ConnectNodes();
NodeId avanodeid = NO_NODE;
// Expire requests after some time.
for (int i = 0; i < 10; i++) {
Response resp = {getRound(), 0, {Vote(0, itemid)}};
avanodeid = getSuitableNodeToQuery();
auto start = std::chrono::steady_clock::now();
runEventLoop();
// We cannot guarantee that we'll wait for just 1ms, so we have to bail
// if we aren't within the proper time range.
std::this_thread::sleep_for(std::chrono::milliseconds(1));
runEventLoop();
std::vector<avalanche::VoteItemUpdate> updates;
bool ret = registerVotes(avanodeid, next(resp), updates);
if (std::chrono::steady_clock::now() > start + queryTimeDuration) {
// We waited for too long, bail. Because we can't know for sure when
// previous steps ran, ret is not deterministic and we do not check
// it.
i--;
continue;
}
// We are within time bounds, so the vote should have worked.
BOOST_CHECK(ret);
avanodeid = getSuitableNodeToQuery();
// Now try again but wait for expiration.
runEventLoop();
std::this_thread::sleep_for(queryTimeDuration);
runEventLoop();
BOOST_CHECK(!registerVotes(avanodeid, next(resp), updates));
}
}
BOOST_AUTO_TEST_CASE_TEMPLATE(poll_inflight_count, P, VoteItemProviders) {
P provider(this);
const uint32_t invType = provider.invType;
// Create enough nodes so that we run into the inflight request limit.
auto proof = GetProof();
BOOST_CHECK(m_processor->withPeerManager(
[&](avalanche::PeerManager &pm) { return pm.registerProof(proof); }));
std::array<CNode *, AVALANCHE_MAX_INFLIGHT_POLL + 1> nodes;
for (auto &n : nodes) {
n = ConnectNode(NODE_AVALANCHE);
BOOST_CHECK(addNode(n->GetId(), proof->getId()));
}
// Add an item to poll
const auto item = provider.buildVoteItem();
const auto itemid = provider.getVoteItemId(item);
- BOOST_CHECK(provider.addToReconcile(item));
+ BOOST_CHECK(addToReconcile(item));
// Ensure there are enough requests in flight.
std::map<NodeId, uint64_t> node_round_map;
for (int i = 0; i < AVALANCHE_MAX_INFLIGHT_POLL; i++) {
NodeId nodeid = getSuitableNodeToQuery();
BOOST_CHECK(node_round_map.find(nodeid) == node_round_map.end());
node_round_map.insert(std::pair<NodeId, uint64_t>(nodeid, getRound()));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
runEventLoop();
}
// Now that we have enough in flight requests, we shouldn't poll.
auto suitablenodeid = getSuitableNodeToQuery();
BOOST_CHECK(suitablenodeid != NO_NODE);
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
runEventLoop();
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), suitablenodeid);
// Send one response, now we can poll again.
auto it = node_round_map.begin();
Response resp = {it->second, 0, {Vote(0, itemid)}};
std::vector<avalanche::VoteItemUpdate> updates;
BOOST_CHECK(registerVotes(it->first, resp, updates));
node_round_map.erase(it);
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
}
BOOST_AUTO_TEST_CASE(quorum_diversity) {
std::vector<VoteItemUpdate> updates;
CBlock block = CreateAndProcessBlock({}, CScript());
const BlockHash blockHash = block.GetHash();
const CBlockIndex *pindex;
{
LOCK(cs_main);
pindex =
Assert(m_node.chainman)->m_blockman.LookupBlockIndex(blockHash);
}
// Create nodes that supports avalanche.
auto avanodes = ConnectNodes();
// Querying for random block returns false.
BOOST_CHECK(!m_processor->isAccepted(pindex));
// Add a new block. Check it is added to the polls.
BOOST_CHECK(m_processor->addToReconcile(pindex));
// Do one valid round of voting.
uint64_t round = getRound();
Response resp{round, 0, {Vote(0, blockHash)}};
// Check that all nodes can vote.
for (size_t i = 0; i < avanodes.size(); i++) {
runEventLoop();
BOOST_CHECK(registerVotes(avanodes[i]->GetId(), next(resp), updates));
}
// Generate a query for every single node.
const NodeId firstNodeId = getSuitableNodeToQuery();
std::map<NodeId, uint64_t> node_round_map;
round = getRound();
for (size_t i = 0; i < avanodes.size(); i++) {
NodeId nodeid = getSuitableNodeToQuery();
BOOST_CHECK(node_round_map.find(nodeid) == node_round_map.end());
node_round_map[nodeid] = getRound();
runEventLoop();
}
// Now only the first node can vote. All others would be duplicate in the
// quorum.
auto confidence = m_processor->getConfidence(pindex);
BOOST_REQUIRE(confidence > 0);
for (auto &[nodeid, r] : node_round_map) {
if (nodeid == firstNodeId) {
// Node 0 is the only one which can vote at this stage.
round = r;
continue;
}
BOOST_CHECK(
registerVotes(nodeid, {r, 0, {Vote(0, blockHash)}}, updates));
BOOST_CHECK_EQUAL(m_processor->getConfidence(pindex), confidence);
}
BOOST_CHECK(
registerVotes(firstNodeId, {round, 0, {Vote(0, blockHash)}}, updates));
BOOST_CHECK_EQUAL(m_processor->getConfidence(pindex), confidence + 1);
}
BOOST_AUTO_TEST_CASE(event_loop) {
CScheduler s;
CBlock block = CreateAndProcessBlock({}, CScript());
const BlockHash blockHash = block.GetHash();
const CBlockIndex *pindex;
{
LOCK(cs_main);
pindex =
Assert(m_node.chainman)->m_blockman.LookupBlockIndex(blockHash);
}
// Starting the event loop.
BOOST_CHECK(m_processor->startEventLoop(s));
// There is one task planned in the next hour (our event loop).
std::chrono::steady_clock::time_point start, stop;
BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 1);
// Starting twice doesn't start it twice.
BOOST_CHECK(!m_processor->startEventLoop(s));
// Start the scheduler thread.
std::thread schedulerThread(std::bind(&CScheduler::serviceQueue, &s));
// Create a quorum of nodes that support avalanche.
auto avanodes = ConnectNodes();
// There is no query in flight at the moment.
NodeId nodeid = getSuitableNodeToQuery();
BOOST_CHECK_NE(nodeid, NO_NODE);
// Add a new block. Check it is added to the polls.
uint64_t queryRound = getRound();
BOOST_CHECK(m_processor->addToReconcile(pindex));
// Wait until all nodes got a poll
for (int i = 0; i < 60 * 1000; i++) {
// Technically, this is a race condition, but this should do just fine
// as we wait up to 1 minute for an event that should take 80ms.
UninterruptibleSleep(std::chrono::milliseconds(1));
if (getRound() == queryRound + avanodes.size()) {
break;
}
}
// Check that we effectively got a request and not timed out.
BOOST_CHECK(getRound() > queryRound);
// Respond and check the cooldown time is respected.
uint64_t responseRound = getRound();
auto queryTime =
std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
std::vector<VoteItemUpdate> updates;
// Only the first node answers, so it's the only one that gets polled again
BOOST_CHECK(registerVotes(nodeid, {queryRound, 100, {Vote(0, blockHash)}},
updates));
for (int i = 0; i < 10000; i++) {
// We make sure that we do not get a request before queryTime.
UninterruptibleSleep(std::chrono::milliseconds(1));
if (getRound() != responseRound) {
BOOST_CHECK(std::chrono::steady_clock::now() > queryTime);
break;
}
}
// But we eventually get one.
BOOST_CHECK(getRound() > responseRound);
// Stop event loop.
BOOST_CHECK(m_processor->stopEventLoop());
// We don't have any task scheduled anymore.
BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 0);
// Can't stop the event loop twice.
BOOST_CHECK(!m_processor->stopEventLoop());
// Wait for the scheduler to stop.
s.StopWhenDrained();
schedulerThread.join();
}
BOOST_AUTO_TEST_CASE(destructor) {
CScheduler s;
std::chrono::steady_clock::time_point start, stop;
std::thread schedulerThread;
BOOST_CHECK(m_processor->startEventLoop(s));
BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 1);
// Start the service thread after the queue size check to prevent a race
// condition where the thread may be processing the event loop task during
// the check.
schedulerThread = std::thread(std::bind(&CScheduler::serviceQueue, &s));
// Destroy the processor.
m_processor.reset();
// Now that avalanche is destroyed, there is no more scheduled tasks.
BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 0);
// Wait for the scheduler to stop.
s.StopWhenDrained();
schedulerThread.join();
}
BOOST_AUTO_TEST_CASE(add_proof_to_reconcile) {
uint32_t score = MIN_VALID_PROOF_SCORE;
CChainState &active_chainstate =
Assert(m_node.chainman)->ActiveChainstate();
auto addProofToReconcile = [&](uint32_t proofScore) {
auto proof = buildRandomProof(active_chainstate, proofScore);
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof));
});
BOOST_CHECK(m_processor->addToReconcile(proof));
return proof;
};
for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) {
auto proof = addProofToReconcile(++score);
auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
BOOST_CHECK_EQUAL(invs.size(), i + 1);
BOOST_CHECK(invs.front().IsMsgProof());
BOOST_CHECK_EQUAL(invs.front().hash, proof->getId());
}
// From here a new proof is only polled if its score is in the top
// AVALANCHE_MAX_ELEMENT_POLL
ProofId lastProofId;
for (size_t i = 0; i < 10; i++) {
auto proof = addProofToReconcile(++score);
auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
BOOST_CHECK_EQUAL(invs.size(), AVALANCHE_MAX_ELEMENT_POLL);
BOOST_CHECK(invs.front().IsMsgProof());
BOOST_CHECK_EQUAL(invs.front().hash, proof->getId());
lastProofId = proof->getId();
}
for (size_t i = 0; i < 10; i++) {
auto proof = addProofToReconcile(--score);
auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
BOOST_CHECK_EQUAL(invs.size(), AVALANCHE_MAX_ELEMENT_POLL);
BOOST_CHECK(invs.front().IsMsgProof());
BOOST_CHECK_EQUAL(invs.front().hash, lastProofId);
}
{
// The score is not high enough to get polled
auto proof = addProofToReconcile(--score);
auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
for (auto &inv : invs) {
BOOST_CHECK_NE(inv.hash, proof->getId());
}
}
}
BOOST_AUTO_TEST_CASE(proof_record) {
setArg("-avaproofstakeutxoconfirmations", "2");
setArg("-avalancheconflictingproofcooldown", "0");
BOOST_CHECK(!m_processor->isAccepted(nullptr));
BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1);
const CKey key = CKey::MakeCompressedKey();
const COutPoint conflictingOutpoint{TxId(GetRandHash()), 0};
const COutPoint immatureOutpoint{TxId(GetRandHash()), 0};
{
CScript script = GetScriptForDestination(PKHash(key.GetPubKey()));
LOCK(cs_main);
CCoinsViewCache &coins =
Assert(m_node.chainman)->ActiveChainstate().CoinsTip();
coins.AddCoin(conflictingOutpoint,
Coin(CTxOut(PROOF_DUST_THRESHOLD, script), 10, false),
false);
coins.AddCoin(immatureOutpoint,
Coin(CTxOut(PROOF_DUST_THRESHOLD, script), 100, false),
false);
}
auto buildProof = [&](const COutPoint &outpoint, uint64_t sequence,
uint32_t height = 10) {
ProofBuilder pb(sequence, 0, key, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
BOOST_CHECK(
pb.addUTXO(outpoint, PROOF_DUST_THRESHOLD, height, false, key));
return pb.build();
};
auto conflictingProof = buildProof(conflictingOutpoint, 1);
auto validProof = buildProof(conflictingOutpoint, 2);
auto immatureProof = buildProof(immatureOutpoint, 3, 100);
BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
BOOST_CHECK(!m_processor->isAccepted(validProof));
BOOST_CHECK(!m_processor->isAccepted(immatureProof));
BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), -1);
BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), -1);
BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
// Reconciling proofs that don't exist will fail
BOOST_CHECK(!m_processor->addToReconcile(conflictingProof));
BOOST_CHECK(!m_processor->addToReconcile(validProof));
BOOST_CHECK(!m_processor->addToReconcile(immatureProof));
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(conflictingProof));
BOOST_CHECK(pm.registerProof(validProof));
BOOST_CHECK(!pm.registerProof(immatureProof));
BOOST_CHECK(pm.isBoundToPeer(validProof->getId()));
BOOST_CHECK(pm.isInConflictingPool(conflictingProof->getId()));
BOOST_CHECK(pm.isImmature(immatureProof->getId()));
});
BOOST_CHECK(m_processor->addToReconcile(conflictingProof));
BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
BOOST_CHECK(!m_processor->isAccepted(validProof));
BOOST_CHECK(!m_processor->isAccepted(immatureProof));
BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0);
BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), -1);
BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
BOOST_CHECK(m_processor->addToReconcile(validProof));
BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
BOOST_CHECK(m_processor->isAccepted(validProof));
BOOST_CHECK(!m_processor->isAccepted(immatureProof));
BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0);
BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), 0);
BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
BOOST_CHECK(!m_processor->addToReconcile(immatureProof));
BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
BOOST_CHECK(m_processor->isAccepted(validProof));
BOOST_CHECK(!m_processor->isAccepted(immatureProof));
BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0);
BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), 0);
BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
}
BOOST_AUTO_TEST_CASE(quorum_detection) {
// Set min quorum parameters for our test
int minStake = 400'000'000;
setArg("-avaminquorumstake", ToString(minStake));
setArg("-avaminquorumconnectedstakeratio", "0.5");
// Create a new processor with our given quorum parameters
const auto currency = Currency::get();
uint32_t minScore = Proof::amountToScore(minStake * currency.baseunit);
CChainState &active_chainstate =
Assert(m_node.chainman)->ActiveChainstate();
const CKey key = CKey::MakeCompressedKey();
auto localProof =
buildRandomProof(active_chainstate, minScore / 4, 100, key);
setArg("-avamasterkey", EncodeSecret(key));
setArg("-avaproof", localProof->ToHex());
bilingual_str error;
ChainstateManager &chainman = *Assert(m_node.chainman);
m_processor = Processor::MakeProcessor(*m_node.args, *m_node.chain,
m_node.connman.get(), chainman,
*m_node.scheduler, error);
BOOST_CHECK(m_processor != nullptr);
BOOST_CHECK(m_processor->getLocalProof() != nullptr);
BOOST_CHECK_EQUAL(m_processor->getLocalProof()->getId(),
localProof->getId());
BOOST_CHECK_EQUAL(AvalancheTest::getMinQuorumScore(*m_processor), minScore);
BOOST_CHECK_EQUAL(
AvalancheTest::getMinQuorumConnectedScoreRatio(*m_processor), 0.5);
// The local proof has not been validated yet
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 0);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0);
});
BOOST_CHECK(!m_processor->isQuorumEstablished());
// Register the local proof. This is normally done when the chain tip is
// updated. The local proof should be accounted for in the min quorum
// computation but the peer manager doesn't know about that.
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(m_processor->getLocalProof()));
BOOST_CHECK(pm.isBoundToPeer(m_processor->getLocalProof()->getId()));
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0);
});
BOOST_CHECK(!m_processor->isQuorumEstablished());
// Add enough nodes to get a conclusive vote
for (NodeId id = 0; id < 8; id++) {
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.addNode(id, m_processor->getLocalProof()->getId());
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
}
// Add part of the required stake and make sure we still report no quorum
auto proof1 = buildRandomProof(active_chainstate, minScore / 2);
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof1));
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 3 * minScore / 4);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
BOOST_CHECK(!m_processor->isQuorumEstablished());
// Add the rest of the stake, but we are still lacking connected stake
const int64_t tipTime = chainman.ActiveTip()->GetBlockTime();
const COutPoint utxo{TxId(GetRandHash()), 0};
const Amount amount = (int64_t(minScore / 4) * COIN) / 100;
const int height = 100;
const bool isCoinbase = false;
{
LOCK(cs_main);
CCoinsViewCache &coins = active_chainstate.CoinsTip();
coins.AddCoin(utxo,
Coin(CTxOut(amount, GetScriptForDestination(
PKHash(key.GetPubKey()))),
height, isCoinbase),
false);
}
ProofBuilder pb(1, tipTime + 1, key, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
BOOST_CHECK(pb.addUTXO(utxo, amount, height, isCoinbase, key));
auto proof2 = pb.build();
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof2));
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
BOOST_CHECK(!m_processor->isQuorumEstablished());
// Adding a node should cause the quorum to be detected and locked-in
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.addNode(8, proof2->getId());
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore);
// The peer manager knows that proof2 has a node attached ...
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 2);
});
// ... but the processor also account for the local proof, so we reached 50%
BOOST_CHECK(m_processor->isQuorumEstablished());
// Go back to not having enough connected score, but we've already latched
// the quorum as established
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.removeNode(8);
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
BOOST_CHECK(m_processor->isQuorumEstablished());
// Removing one more node drops our count below the minimum and the quorum
// is no longer ready
m_processor->withPeerManager(
[&](avalanche::PeerManager &pm) { pm.removeNode(7); });
BOOST_CHECK(!m_processor->isQuorumEstablished());
// It resumes when we have enough nodes again
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.addNode(7, m_processor->getLocalProof()->getId());
});
BOOST_CHECK(m_processor->isQuorumEstablished());
// Remove peers one at a time until the quorum is no longer established
auto spendProofUtxo = [&](ProofRef proof) {
{
LOCK(cs_main);
CCoinsViewCache &coins = chainman.ActiveChainstate().CoinsTip();
coins.SpendCoin(proof->getStakes()[0].getStake().getUTXO());
}
m_processor->withPeerManager([&proof](avalanche::PeerManager &pm) {
pm.updatedBlockTip();
BOOST_CHECK(!pm.isBoundToPeer(proof->getId()));
});
};
// Expire proof2, the quorum is still latched
for (int64_t i = 0; i < 6; i++) {
SetMockTime(proof2->getExpirationTime() + i);
CreateAndProcessBlock({}, CScript());
}
BOOST_CHECK_EQUAL(chainman.ActiveTip()->GetMedianTimePast(),
proof2->getExpirationTime());
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.updatedBlockTip();
BOOST_CHECK(!pm.exists(proof2->getId()));
});
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 3 * minScore / 4);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
BOOST_CHECK(m_processor->isQuorumEstablished());
spendProofUtxo(proof1);
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
BOOST_CHECK(m_processor->isQuorumEstablished());
spendProofUtxo(m_processor->getLocalProof());
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 0);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0);
});
// There is no node left
BOOST_CHECK(!m_processor->isQuorumEstablished());
}
BOOST_AUTO_TEST_CASE(quorum_detection_parameter_validation) {
// Create vector of tuples of:
// <min stake, min ratio, min avaproofs messages, success bool>
const std::vector<std::tuple<std::string, std::string, std::string, bool>>
testCases = {
// All parameters are invalid
{"", "", "", false},
{"-1", "-1", "-1", false},
// Min stake is out of range
{"-1", "0", "0", false},
{"-0.01", "0", "0", false},
{"21000000000000.01", "0", "0", false},
// Min connected ratio is out of range
{"0", "-1", "0", false},
{"0", "1.1", "0", false},
// Min avaproofs messages ratio is out of range
{"0", "0", "-1", false},
// All parameters are valid
{"0", "0", "0", true},
{"0.00", "0", "0", true},
{"0.01", "0", "0", true},
{"1", "0.1", "0", true},
{"10", "0.5", "0", true},
{"10", "1", "0", true},
{"21000000000000.00", "0", "0", true},
{"0", "0", "1", true},
{"0", "0", "100", true},
};
// For each case set the parameters and check that making the processor
// succeeds or fails as expected
for (const auto &[stake, stakeRatio, numProofsMessages, success] :
testCases) {
setArg("-avaminquorumstake", stake);
setArg("-avaminquorumconnectedstakeratio", stakeRatio);
setArg("-avaminavaproofsnodecount", numProofsMessages);
bilingual_str error;
std::unique_ptr<Processor> processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(),
*Assert(m_node.chainman), *m_node.scheduler, error);
if (success) {
BOOST_CHECK(processor != nullptr);
BOOST_CHECK(error.empty());
BOOST_CHECK_EQUAL(error.original, "");
} else {
BOOST_CHECK(processor == nullptr);
BOOST_CHECK(!error.empty());
BOOST_CHECK(error.original != "");
}
}
}
BOOST_AUTO_TEST_CASE(min_avaproofs_messages) {
setArg("-avaminquorumstake", "0");
setArg("-avaminquorumconnectedstakeratio", "0");
ChainstateManager &chainman = *Assert(m_node.chainman);
auto checkMinAvaproofsMessages = [&](int64_t minAvaproofsMessages) {
setArg("-avaminavaproofsnodecount", ToString(minAvaproofsMessages));
bilingual_str error;
auto processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(), chainman,
*m_node.scheduler, error);
auto addNode = [&](NodeId nodeid) {
auto proof = buildRandomProof(chainman.ActiveChainstate(),
MIN_VALID_PROOF_SCORE);
processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof));
BOOST_CHECK(pm.addNode(nodeid, proof->getId()));
});
};
// Add enough node to have a conclusive vote, but don't account any
// avaproofs.
// NOTE: we can't use the test facilites like ConnectNodes() because we
// are not testing on m_processor.
for (NodeId id = 100; id < 108; id++) {
addNode(id);
}
BOOST_CHECK_EQUAL(processor->isQuorumEstablished(),
minAvaproofsMessages <= 0);
for (int64_t i = 0; i < minAvaproofsMessages - 1; i++) {
addNode(i);
processor->avaproofsSent(i);
BOOST_CHECK_EQUAL(processor->getAvaproofsNodeCounter(), i + 1);
// Receiving again on the same node does not increase the counter
processor->avaproofsSent(i);
BOOST_CHECK_EQUAL(processor->getAvaproofsNodeCounter(), i + 1);
BOOST_CHECK(!processor->isQuorumEstablished());
}
addNode(minAvaproofsMessages);
processor->avaproofsSent(minAvaproofsMessages);
BOOST_CHECK(processor->isQuorumEstablished());
// Check the latch
AvalancheTest::clearavaproofsNodeCounter(*processor);
BOOST_CHECK(processor->isQuorumEstablished());
};
checkMinAvaproofsMessages(0);
checkMinAvaproofsMessages(1);
checkMinAvaproofsMessages(10);
checkMinAvaproofsMessages(100);
}
BOOST_AUTO_TEST_CASE_TEMPLATE(voting_parameters, P, VoteItemProviders) {
// Check that setting voting parameters has the expected effect
setArg("-avastalevotethreshold",
ToString(AVALANCHE_VOTE_STALE_MIN_THRESHOLD));
setArg("-avastalevotefactor", "2");
const std::vector<std::tuple<int, int>> testCases = {
// {number of yes votes, number of neutral votes}
{0, AVALANCHE_VOTE_STALE_MIN_THRESHOLD},
{AVALANCHE_FINALIZATION_SCORE + 4, AVALANCHE_FINALIZATION_SCORE - 6},
};
bilingual_str error;
m_processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(),
*Assert(m_node.chainman), *m_node.scheduler, error);
BOOST_CHECK(m_processor != nullptr);
BOOST_CHECK(error.empty());
P provider(this);
const uint32_t invType = provider.invType;
const auto item = provider.buildVoteItem();
const auto itemid = provider.getVoteItemId(item);
// Create nodes that supports avalanche.
auto avanodes = ConnectNodes();
int nextNodeIndex = 0;
std::vector<avalanche::VoteItemUpdate> updates;
for (const auto &[numYesVotes, numNeutralVotes] : testCases) {
// Add a new item. Check it is added to the polls.
- BOOST_CHECK(provider.addToReconcile(item));
+ BOOST_CHECK(addToReconcile(item));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
BOOST_CHECK(m_processor->isAccepted(item));
auto registerNewVote = [&](const Response &resp) {
runEventLoop();
auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
BOOST_CHECK(registerVotes(nodeid, resp, updates));
};
// Add some confidence
for (int i = 0; i < numYesVotes; i++) {
Response resp = {getRound(), 0, {Vote(0, itemid)}};
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item),
i >= 6 ? i - 5 : 0);
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// Vote until just before item goes stale
for (int i = 0; i < numNeutralVotes; i++) {
Response resp = {getRound(), 0, {Vote(-1, itemid)}};
registerNewVote(next(resp));
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// As long as it is not stale, we poll.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
// Now stale
Response resp = {getRound(), 0, {Vote(-1, itemid)}};
registerNewVote(next(resp));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Stale);
updates.clear();
// Once stale, there is no poll for it.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
}
}
BOOST_AUTO_TEST_CASE(block_vote_finalization_tip) {
BlockProvider provider(this);
std::vector<CBlockIndex *> blockIndexes;
for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) {
CBlockIndex *pindex = provider.buildVoteItem();
- BOOST_CHECK(provider.addToReconcile(pindex));
+ BOOST_CHECK(addToReconcile(pindex));
blockIndexes.push_back(pindex);
}
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), AVALANCHE_MAX_ELEMENT_POLL);
for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) {
BOOST_CHECK_EQUAL(
invs[i].hash,
blockIndexes[AVALANCHE_MAX_ELEMENT_POLL - i - 1]->GetBlockHash());
}
// Build a vote vector with the 11th block only being accepted and others
// unknown.
const BlockHash eleventhBlockHash =
blockIndexes[AVALANCHE_MAX_ELEMENT_POLL - 10 - 1]->GetBlockHash();
std::vector<Vote> votes;
votes.reserve(AVALANCHE_MAX_ELEMENT_POLL);
for (size_t i = AVALANCHE_MAX_ELEMENT_POLL; i > 0; i--) {
BlockHash blockhash = blockIndexes[i - 1]->GetBlockHash();
votes.emplace_back(blockhash == eleventhBlockHash ? 0 : -1, blockhash);
}
auto avanodes = ConnectNodes();
int nextNodeIndex = 0;
std::vector<avalanche::VoteItemUpdate> updates;
auto registerNewVote = [&]() {
Response resp = {getRound(), 0, votes};
runEventLoop();
auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
BOOST_CHECK(registerVotes(nodeid, resp, updates));
};
// Vote for the blocks until the one being accepted finalizes
bool eleventhBlockFinalized = false;
for (size_t i = 0; i < 10000 && !eleventhBlockFinalized; i++) {
registerNewVote();
for (auto &update : updates) {
if (update.getStatus() == VoteStatus::Finalized &&
provider.fromAnyVoteItem(update.getVoteItem())
->GetBlockHash() == eleventhBlockHash) {
eleventhBlockFinalized = true;
}
}
}
BOOST_CHECK(eleventhBlockFinalized);
// From now only the 10 blocks with more work are polled for
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 10);
for (size_t i = 0; i < 10; i++) {
BOOST_CHECK_EQUAL(
invs[i].hash,
blockIndexes[AVALANCHE_MAX_ELEMENT_POLL - i - 1]->GetBlockHash());
}
// Adding ancestor blocks to reconcile will fail
for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL - 10 - 1; i++) {
- BOOST_CHECK(!provider.addToReconcile(blockIndexes[i]));
+ BOOST_CHECK(!addToReconcile(blockIndexes[i]));
}
// Create a couple concurrent chain tips
CBlockIndex *tip = provider.buildVoteItem();
const auto &config = GetConfig();
auto &activeChainstate = m_node.chainman->ActiveChainstate();
BlockValidationState state;
activeChainstate.InvalidateBlock(config, state, tip);
// Use another script to make sure we don't generate the same block again
CBlock altblock = CreateAndProcessBlock({}, CScript() << OP_TRUE);
auto alttip = WITH_LOCK(
cs_main, return Assert(m_node.chainman)
->m_blockman.LookupBlockIndex(altblock.GetHash()));
BOOST_CHECK(alttip);
BOOST_CHECK(alttip->pprev == tip->pprev);
BOOST_CHECK(alttip->GetBlockHash() != tip->GetBlockHash());
// Reconsider the previous tip valid, so we have concurrent tip candidates
{
LOCK(cs_main);
activeChainstate.ResetBlockFailureFlags(tip);
}
activeChainstate.ActivateBestChain(config, state);
- BOOST_CHECK(provider.addToReconcile(tip));
- BOOST_CHECK(provider.addToReconcile(alttip));
+ BOOST_CHECK(addToReconcile(tip));
+ BOOST_CHECK(addToReconcile(alttip));
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 12);
// Vote for the tip until it finalizes
BlockHash tiphash = tip->GetBlockHash();
votes.clear();
votes.reserve(12);
for (auto &inv : invs) {
votes.emplace_back(inv.hash == tiphash ? 0 : -1, inv.hash);
}
bool tipFinalized = false;
for (size_t i = 0; i < 10000 && !tipFinalized; i++) {
registerNewVote();
for (auto &update : updates) {
if (update.getStatus() == VoteStatus::Finalized &&
provider.fromAnyVoteItem(update.getVoteItem())
->GetBlockHash() == tiphash) {
tipFinalized = true;
}
}
}
BOOST_CHECK(tipFinalized);
// Now the tip and all its ancestors will be removed from polls. Only the
// alttip remains because it is on a forked chain so we want to keep polling
// for that one until it's invalidated or stalled.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].hash, alttip->GetBlockHash());
}
BOOST_AUTO_TEST_CASE(vote_map_comparator) {
ChainstateManager &chainman = *Assert(m_node.chainman);
CChainState &activeChainState = chainman.ActiveChainstate();
const int numberElementsEachType = 100;
FastRandomContext rng;
std::vector<ProofRef> proofs;
for (size_t i = 1; i <= numberElementsEachType; i++) {
auto proof =
buildRandomProof(activeChainState, i * MIN_VALID_PROOF_SCORE);
BOOST_CHECK(proof != nullptr);
proofs.emplace_back(std::move(proof));
}
Shuffle(proofs.begin(), proofs.end(), rng);
std::vector<CBlockIndex> indexes;
for (size_t i = 1; i <= numberElementsEachType; i++) {
CBlockIndex index;
index.nChainWork = i;
indexes.emplace_back(std::move(index));
}
Shuffle(indexes.begin(), indexes.end(), rng);
auto allItems = std::make_tuple(std::move(proofs), std::move(indexes));
static const size_t numTypes = std::tuple_size<decltype(allItems)>::value;
RWCollection<VoteMap> voteMap;
{
auto writeView = voteMap.getWriteView();
for (size_t i = 0; i < numberElementsEachType; i++) {
// Randomize the insert order at each loop increment
const size_t firstType = rng.randrange(numTypes);
for (size_t j = 0; j < numTypes; j++) {
switch ((firstType + j) % numTypes) {
// ProofRef
case 0:
writeView->insert(std::make_pair(
std::get<0>(allItems)[i], VoteRecord(true)));
break;
// CBlockIndex *
case 1:
writeView->insert(std::make_pair(
&std::get<1>(allItems)[i], VoteRecord(true)));
break;
default:
break;
}
}
}
}
{
// Check ordering
auto readView = voteMap.getReadView();
auto it = readView.begin();
// The first batch of items is the proofs ordered by score (descending)
uint32_t lastScore = std::numeric_limits<uint32_t>::max();
for (size_t i = 0; i < numberElementsEachType; i++) {
BOOST_CHECK(std::holds_alternative<const ProofRef>(it->first));
uint32_t currentScore =
std::get<const ProofRef>(it->first)->getScore();
BOOST_CHECK_LT(currentScore, lastScore);
lastScore = currentScore;
it++;
}
// The next batch of items is the block indexes ordered by work
// (descending)
arith_uint256 lastWork = -1;
for (size_t i = 0; i < numberElementsEachType; i++) {
BOOST_CHECK(std::holds_alternative<const CBlockIndex *>(it->first));
arith_uint256 currentWork =
std::get<const CBlockIndex *>(it->first)->nChainWork;
BOOST_CHECK(currentWork < lastWork);
lastWork = currentWork;
it++;
}
BOOST_CHECK(it == readView.end());
}
}
BOOST_AUTO_TEST_CASE(block_reconcile_initial_vote) {
const auto &config = GetConfig();
auto &chainman = Assert(m_node.chainman);
CChainState &chainstate = chainman->ActiveChainstate();
const auto block = std::make_shared<const CBlock>(
this->CreateBlock({}, CScript(), chainstate));
const BlockHash blockhash = block->GetHash();
BlockValidationState state;
CBlockIndex *blockindex;
{
LOCK(cs_main);
BOOST_CHECK(chainstate.AcceptBlock(config, block, state,
/*fRequested=*/true, /*dbp=*/nullptr,
/*fNewBlock=*/nullptr));
blockindex = chainman->m_blockman.LookupBlockIndex(blockhash);
BOOST_CHECK(blockindex);
}
// ActivateBestChain() interacts with g_avalanche, so make it happy
g_avalanche = std::move(m_processor);
// The block is not connected yet, and not added to the poll list yet
BOOST_CHECK(AvalancheTest::getInvsForNextPoll(*g_avalanche).empty());
BOOST_CHECK(!g_avalanche->isAccepted(blockindex));
// Call ActivateBestChain to connect the new block
BOOST_CHECK(chainstate.ActivateBestChain(config, state, block));
// It is a valid block so the tip is updated
BOOST_CHECK_EQUAL(chainstate.m_chain.Tip(), blockindex);
// Check the block is added to the poll
auto invs = AvalancheTest::getInvsForNextPoll(*g_avalanche);
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, MSG_BLOCK);
BOOST_CHECK_EQUAL(invs[0].hash, blockhash);
// This block is our new tip so we should vote "yes"
BOOST_CHECK(g_avalanche->isAccepted(blockindex));
// Prevent a data race between UpdatedBlockTip and the Processor destructor
SyncWithValidationInterfaceQueue();
g_avalanche.reset(nullptr);
}
BOOST_AUTO_TEST_SUITE_END()

File Metadata

Mime Type
text/x-diff
Expires
Wed, May 21, 23:34 (1 d, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5866156
Default Alt Text
(71 KB)

Event Timeline