diff --git a/src/avalanche.h b/src/avalanche.h --- a/src/avalanche.h +++ b/src/avalanche.h @@ -150,11 +150,33 @@ class AvalancheProcessor { private: + CConnman *connman; + /** * Blocks to run avalanche on. */ RWCollection> vote_records; + /** + * Keep track of peers and queries sent. + */ + struct RequestRecord { + private: + int64_t timestamp; + std::vector invs; + + public: + RequestRecord() : timestamp(0), invs() {} + RequestRecord(int64_t timestampIn, std::vector invIn) + : timestamp(timestampIn), invs(std::move(invIn)) {} + + int64_t GetTimestamp() const { return timestamp; } + const std::vector &GetInvs() const { return invs; } + }; + + RWCollection> nodeids; + RWCollection> queries; + /** * Start stop machinery. */ @@ -165,13 +187,14 @@ std::condition_variable cond_running; public: - AvalancheProcessor() : stopRequest(false), running(false) {} + AvalancheProcessor(CConnman *connmanIn) + : connman(connmanIn), stopRequest(false), running(false) {} ~AvalancheProcessor() { stopEventLoop(); } bool addBlockToReconcile(const CBlockIndex *pindex); bool isAccepted(const CBlockIndex *pindex) const; - bool registerVotes(const AvalancheResponse &response, + bool registerVotes(NodeId nodeid, const AvalancheResponse &response, std::vector &accepted, std::vector &rejected); @@ -181,6 +204,7 @@ private: void runEventLoop(); std::vector getInvsForNextPoll() const; + NodeId getSuitableNodeToQuery(); friend struct AvalancheTest; }; diff --git a/src/avalanche.cpp b/src/avalanche.cpp --- a/src/avalanche.cpp +++ b/src/avalanche.cpp @@ -34,38 +34,76 @@ return false; } -bool AvalancheProcessor::registerVotes(const AvalancheResponse &response, +bool AvalancheProcessor::registerVotes(NodeId nodeid, + const AvalancheResponse &response, std::vector &accepted, std::vector &rejected) { - const std::vector &votes = response.GetVotes(); + RequestRecord r; - // Register votes. - auto w = vote_records.getWriteView(); - for (auto &v : votes) { - auto it = w->find(v.GetHash()); + { + // Check that the query exists. + auto w = queries.getWriteView(); + auto it = w->find(nodeid); if (it == w.end()) { - // We are not voting on that item anymore. - continue; + // NB: The request may be old, so we don't increase banscore. + return false; } - auto &vr = it->second; - vr.registerVote(v.IsValid()); - if (!vr.hasFinalized()) { - // This item has note been finalized, so we have nothing more to do. - continue; - } + r = std::move(it->second); + w->erase(it); + } - // We just finalized a vote. If it is valid, then let the caller know. - // Either way, remove the item from the map. - if (vr.isValid()) { - accepted.push_back(v.GetHash()); - } else { - rejected.push_back(v.GetHash()); + // Verify that the request and the vote are consistent. + const std::vector &invs = r.GetInvs(); + const std::vector &votes = response.GetVotes(); + size_t size = invs.size(); + if (votes.size() != size) { + // TODO: increase banscore for inconsistent response. + // NB: This isn't timeout but actually node misbehaving. + return false; + } + + for (size_t i = 0; i < size; i++) { + if (invs[i].hash != votes[i].GetHash()) { + // TODO: increase banscore for inconsistent response. + // NB: This isn't timeout but actually node misbehaving. + return false; } + } - w->erase(it); + { + // Register votes. + auto w = vote_records.getWriteView(); + for (auto &v : votes) { + auto it = w->find(v.GetHash()); + if (it == w.end()) { + // We are not voting on that item anymore. + continue; + } + + auto &vr = it->second; + vr.registerVote(v.IsValid()); + if (!vr.hasFinalized()) { + // This item has note been finalized, so we have nothing more to + // do. + continue; + } + + // We just finalized a vote. If it is valid, then let the caller + // know. Either way, remove the item from the map. + if (vr.isValid()) { + accepted.push_back(v.GetHash()); + } else { + rejected.push_back(v.GetHash()); + } + + w->erase(it); + } } + // Put the node back in the list of queriable nodes. + auto w = nodeids.getWriteView(); + w->insert(nodeid); return true; } @@ -92,6 +130,7 @@ // Start the event loop. scheduler.scheduleEvery( [this]() -> bool { + runEventLoop(); if (!stopRequest) { return true; } @@ -148,6 +187,65 @@ return invs; } +NodeId AvalancheProcessor::getSuitableNodeToQuery() { + auto w = nodeids.getWriteView(); + if (w->empty()) { + auto r = queries.getReadView(); + + // We don't have any candidate node, so let's try to find some. + connman->ForEachNode([&w, &r](CNode *pnode) { + // If this node doesn't support avalanche, we remove. + if (!(pnode->nServices & NODE_AVALANCHE)) { + return; + } + + // if we have a request in flight for that node. + if (r->find(pnode->GetId()) != r.end()) { + return; + } + + w->insert(pnode->GetId()); + }); + } + + // We don't have any suitable candidate. + if (w->empty()) { + return -1; + } + + auto it = w.begin(); + NodeId nodeid = *it; + w->erase(it); + + return nodeid; +} + void AvalancheProcessor::runEventLoop() { std::vector invs = getInvsForNextPoll(); + if (invs.empty()) { + // If there are no invs to poll, we are done. + return; + } + + NodeId nodeid = getSuitableNodeToQuery(); + + /** + * If we lost contact to that node, then we remove it from nodeids, but + * never add the request to queries, which ensures bad nodes get cleaned up + * over time. + */ + connman->ForNode(nodeid, [this, &invs](CNode *pfrom) { + { + // Register the query. + queries.getWriteView()->emplace( + pfrom->GetId(), RequestRecord(GetAdjustedTime(), invs)); + } + + // Send the query to the node. + connman->PushMessage( + pfrom, + CNetMsgMaker(pfrom->GetSendVersion()) + .Make(NetMsgType::AVA_POLL, AvalanchePoll(std::move(invs)))); + return true; + }); } diff --git a/src/net.h b/src/net.h --- a/src/net.h +++ b/src/net.h @@ -444,6 +444,7 @@ friend struct CConnmanTest; }; + extern std::unique_ptr g_connman; void Discover(); void StartMapPort(); diff --git a/src/protocol.h b/src/protocol.h --- a/src/protocol.h +++ b/src/protocol.h @@ -252,6 +252,16 @@ * @since protocol version 70014 as described by BIP 152 */ extern const char *BLOCKTXN; +/** + * Contains a AvalanchePoll. + * Peer should respond with "ava_vote" message. + */ +extern const char *AVA_POLL; +/** + * Contains a vector of AvalancheVote. + * Sent in response to a "ava_poll" message. + */ +extern const char *AVA_VOTE; /** * Indicate if the message is used to transmit the content of a block. @@ -294,6 +304,9 @@ // TODO: remove (free up) the NODE_BITCOIN_CASH service bit once no longer // needed. NODE_BITCOIN_CASH = (1 << 5), + // NODE_AVALANCHE means the node supports Bitcoin Cash's avalanche + // preconsensus mechanism. + NODE_AVALANCHE = (1 << 6), // Bits 24-31 are reserved for temporary experiments. Just pick a bit that // isn't getting used, or one not being used much, and notify the diff --git a/src/protocol.cpp b/src/protocol.cpp --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -41,6 +41,8 @@ const char *CMPCTBLOCK = "cmpctblock"; const char *GETBLOCKTXN = "getblocktxn"; const char *BLOCKTXN = "blocktxn"; +const char *AVA_POLL = "ava_poll"; +const char *AVA_VOTE = "ava_vote"; bool IsBlockLike(const std::string &strCommand) { return strCommand == NetMsgType::BLOCK || diff --git a/src/test/avalanche_tests.cpp b/src/test/avalanche_tests.cpp --- a/src/test/avalanche_tests.cpp +++ b/src/test/avalanche_tests.cpp @@ -3,6 +3,9 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "avalanche.h" + +#include "config.h" +#include "net_processing.h" #include "scheduler.h" #include "test/test_bitcoin.h" @@ -10,12 +13,18 @@ #include struct AvalancheTest { + static void runEventLoop(AvalancheProcessor &p) { p.runEventLoop(); } + static std::vector getInvsForNextPoll(const AvalancheProcessor &p) { return p.getInvsForNextPoll(); } + + static NodeId getSuitableNodeToQuery(AvalancheProcessor &p) { + return p.getSuitableNodeToQuery(); + } }; -BOOST_FIXTURE_TEST_SUITE(avalanche_tests, BasicTestingSetup) +BOOST_FIXTURE_TEST_SUITE(avalanche_tests, TestingSetup) #define REGISTER_VOTE_AND_CHECK(vr, vote, state, finalized, confidence) \ vr.registerVote(vote); \ @@ -73,11 +82,42 @@ AVALANCHE_FINALIZATION_SCORE); } +CService ip(uint32_t i) { + struct in_addr s; + s.s_addr = i; + return CService(CNetAddr(s), Params().GetDefaultPort()); +} + +std::unique_ptr ConnectNode(const Config &config, ServiceFlags nServices, + PeerLogicValidation &peerLogic) { + static NodeId id = 0; + + CAddress addr(ip(GetRandInt(0xffffffff)), NODE_NONE); + std::unique_ptr nodeptr(new CNode(id++, ServiceFlags(NODE_NETWORK), + 0, INVALID_SOCKET, addr, 0, 0, + CAddress(), "", + /*fInboundIn=*/false)); + CNode &node = *nodeptr; + node.SetSendVersion(PROTOCOL_VERSION); + node.nServices = nServices; + peerLogic.InitializeNode(config, &node); + node.nVersion = 1; + node.fSuccessfullyConnected = true; + + CConnmanTest::AddNode(node); + return nodeptr; +} + BOOST_AUTO_TEST_CASE(block_register) { - AvalancheProcessor p; + AvalancheProcessor p(g_connman.get()); CBlockIndex index; std::vector accepted, rejected; + const Config &config = GetConfig(); + + // Create a node that supports avalanche. + auto avanode = ConnectNode(config, NODE_AVALANCHE, *peerLogic); + NodeId nodeid = avanode->GetId(); // Make sure the block has a hash. static const uint256 blockHash(uint256S( @@ -100,7 +140,8 @@ // Let's vote for this block a few times. AvalancheResponse resp{{AvalancheVote(0, blockHash)}}; for (int i = 0; i < 5; i++) { - p.registerVotes(resp, accepted, rejected); + AvalancheTest::runEventLoop(p); + BOOST_CHECK(p.registerVotes(nodeid, resp, accepted, rejected)); BOOST_CHECK(!p.isAccepted(&index)); BOOST_CHECK_EQUAL(accepted.size(), 0); BOOST_CHECK_EQUAL(rejected.size(), 0); @@ -108,7 +149,8 @@ // Now it is accepeted, but we can vote for it numerous times. for (int i = 0; i < AVALANCHE_FINALIZATION_SCORE; i++) { - p.registerVotes(resp, accepted, rejected); + AvalancheTest::runEventLoop(p); + BOOST_CHECK(p.registerVotes(nodeid, resp, accepted, rejected)); BOOST_CHECK(p.isAccepted(&index)); BOOST_CHECK_EQUAL(accepted.size(), 0); BOOST_CHECK_EQUAL(rejected.size(), 0); @@ -121,7 +163,8 @@ BOOST_CHECK(invs[0].hash == blockHash); // Now finalize the decision. - p.registerVotes(resp, accepted, rejected); + AvalancheTest::runEventLoop(p); + BOOST_CHECK(p.registerVotes(nodeid, resp, accepted, rejected)); BOOST_CHECK_EQUAL(accepted.size(), 1); BOOST_CHECK_EQUAL(rejected.size(), 0); BOOST_CHECK(accepted[0] == blockHash); @@ -141,7 +184,8 @@ // Only 3 here as we don't need to flip state. resp = {{AvalancheVote(1, blockHash)}}; for (int i = 0; i < 3; i++) { - p.registerVotes(resp, accepted, rejected); + AvalancheTest::runEventLoop(p); + BOOST_CHECK(p.registerVotes(nodeid, resp, accepted, rejected)); BOOST_CHECK(!p.isAccepted(&index)); BOOST_CHECK_EQUAL(accepted.size(), 0); BOOST_CHECK_EQUAL(rejected.size(), 0); @@ -149,7 +193,8 @@ // Now it is rejected, but we can vote for it numerous times. for (int i = 0; i < AVALANCHE_FINALIZATION_SCORE; i++) { - p.registerVotes(resp, accepted, rejected); + AvalancheTest::runEventLoop(p); + BOOST_CHECK(p.registerVotes(nodeid, resp, accepted, rejected)); BOOST_CHECK(!p.isAccepted(&index)); BOOST_CHECK_EQUAL(accepted.size(), 0); BOOST_CHECK_EQUAL(rejected.size(), 0); @@ -162,7 +207,8 @@ BOOST_CHECK(invs[0].hash == blockHash); // Now finalize the decision. - p.registerVotes(resp, accepted, rejected); + AvalancheTest::runEventLoop(p); + BOOST_CHECK(p.registerVotes(nodeid, resp, accepted, rejected)); BOOST_CHECK(!p.isAccepted(&index)); BOOST_CHECK_EQUAL(accepted.size(), 0); BOOST_CHECK_EQUAL(rejected.size(), 1); @@ -177,13 +223,20 @@ BOOST_CHECK(p.addBlockToReconcile(&index)); BOOST_CHECK(!p.addBlockToReconcile(&index)); BOOST_CHECK(!p.isAccepted(&index)); + + CConnmanTest::ClearNodes(); } BOOST_AUTO_TEST_CASE(multi_block_register) { - AvalancheProcessor p; + AvalancheProcessor p(g_connman.get()); CBlockIndex indexA, indexB; std::vector accepted, rejected; + const Config &config = GetConfig(); + + // Create a node that supports avalanche. + auto node0 = ConnectNode(config, NODE_AVALANCHE, *peerLogic); + auto node1 = ConnectNode(config, NODE_AVALANCHE, *peerLogic); // Make sure the block has a hash. static const uint256 blockHashA(uint256S( @@ -204,13 +257,15 @@ BOOST_CHECK_EQUAL(invs[0].type, MSG_BLOCK); BOOST_CHECK(invs[0].hash == blockHashA); - AvalancheResponse resp{ - {AvalancheVote(0, blockHashA), AvalancheVote(0, blockHashB)}}; - p.registerVotes(resp, accepted, rejected); + AvalancheTest::runEventLoop(p); + BOOST_CHECK(p.registerVotes( + node0->GetId(), {{AvalancheVote(0, blockHashA)}}, accepted, rejected)); BOOST_CHECK_EQUAL(accepted.size(), 0); BOOST_CHECK_EQUAL(rejected.size(), 0); // Start voing on block B after one vote. + AvalancheResponse resp{ + {AvalancheVote(0, blockHashA), AvalancheVote(0, blockHashB)}}; BOOST_CHECK(p.addBlockToReconcile(&indexB)); invs = AvalancheTest::getInvsForNextPoll(p); BOOST_CHECK_EQUAL(invs.size(), 2); @@ -221,13 +276,19 @@ // Now it is rejected, but we can vote for it numerous times. for (int i = 0; i < AVALANCHE_FINALIZATION_SCORE + 4; i++) { - p.registerVotes(resp, accepted, rejected); + AvalancheTest::runEventLoop(p); + BOOST_CHECK(p.registerVotes(node0->GetId(), resp, accepted, rejected)); BOOST_CHECK_EQUAL(accepted.size(), 0); BOOST_CHECK_EQUAL(rejected.size(), 0); } + // Running two iterration of the event loop so that vote gets triggerd on A + // and B. + AvalancheTest::runEventLoop(p); + AvalancheTest::runEventLoop(p); + // Next vote will finalize block A. - p.registerVotes(resp, accepted, rejected); + BOOST_CHECK(p.registerVotes(node1->GetId(), resp, accepted, rejected)); BOOST_CHECK_EQUAL(accepted.size(), 1); BOOST_CHECK_EQUAL(rejected.size(), 0); BOOST_CHECK(accepted[0] == blockHashA); @@ -240,7 +301,7 @@ BOOST_CHECK(invs[0].hash == blockHashB); // Next vote will finalize block B. - p.registerVotes(resp, accepted, rejected); + BOOST_CHECK(p.registerVotes(node0->GetId(), resp, accepted, rejected)); BOOST_CHECK_EQUAL(accepted.size(), 1); BOOST_CHECK_EQUAL(rejected.size(), 0); BOOST_CHECK(accepted[0] == blockHashB); @@ -249,59 +310,190 @@ // There is nothing left to vote on. invs = AvalancheTest::getInvsForNextPoll(p); BOOST_CHECK_EQUAL(invs.size(), 0); + + CConnmanTest::ClearNodes(); +} + +BOOST_AUTO_TEST_CASE(poll_and_response) { + AvalancheProcessor p(g_connman.get()); + + std::vector accepted, rejected; + const Config &config = GetConfig(); + + // Let's register a block to vote on and do one round of event loop. + static const uint256 blockHash(uint256S( + "abcdef0000000000000000000000000000000000000000000000000000000001")); + CBlockIndex index; + index.phashBlock = &blockHash; + + // There is no node to query. + BOOST_CHECK_EQUAL(AvalancheTest::getSuitableNodeToQuery(p), -1); + + // Create a node that supports avalanche and one that doesn't. + auto oldnode = ConnectNode(config, NODE_NONE, *peerLogic); + auto avanode = ConnectNode(config, NODE_AVALANCHE, *peerLogic); + NodeId avanodeid = avanode->GetId(); + + // It returns the avalanche peer. + BOOST_CHECK_EQUAL(AvalancheTest::getSuitableNodeToQuery(p), avanodeid); + + // Register a block and check it is added to the list of elements to poll. + BOOST_CHECK(p.addBlockToReconcile(&index)); + auto invs = AvalancheTest::getInvsForNextPoll(p); + BOOST_CHECK_EQUAL(invs.size(), 1); + BOOST_CHECK_EQUAL(invs[0].type, MSG_BLOCK); + BOOST_CHECK(invs[0].hash == blockHash); + + // Trigger a poll on avanode. + AvalancheTest::runEventLoop(p); + + // There is no more suitable peer available, so return nothing. + BOOST_CHECK_EQUAL(AvalancheTest::getSuitableNodeToQuery(p), -1); + + // Respond to the request. + AvalancheResponse resp = {{AvalancheVote(0, blockHash)}}; + BOOST_CHECK(p.registerVotes(avanode->GetId(), resp, accepted, rejected)); + BOOST_CHECK_EQUAL(accepted.size(), 0); + BOOST_CHECK_EQUAL(rejected.size(), 0); + + // Now that avanode fullfilled his request, it is added back to the list of + // queriable nodes. + BOOST_CHECK_EQUAL(AvalancheTest::getSuitableNodeToQuery(p), avanodeid); + + // Sending a response when not polled fails. + BOOST_CHECK(!p.registerVotes(avanode->GetId(), resp, accepted, rejected)); + BOOST_CHECK_EQUAL(accepted.size(), 0); + BOOST_CHECK_EQUAL(rejected.size(), 0); + + // Trigger a poll on avanode. + AvalancheTest::runEventLoop(p); + BOOST_CHECK_EQUAL(AvalancheTest::getSuitableNodeToQuery(p), -1); + + // Sending responses that do not match the request also fails. + // 1. Too many results. + resp = {{AvalancheVote(0, blockHash), AvalancheVote(0, blockHash)}}; + AvalancheTest::runEventLoop(p); + BOOST_CHECK(!p.registerVotes(avanode->GetId(), resp, accepted, rejected)); + BOOST_CHECK_EQUAL(accepted.size(), 0); + BOOST_CHECK_EQUAL(rejected.size(), 0); + BOOST_CHECK_EQUAL(AvalancheTest::getSuitableNodeToQuery(p), avanodeid); + + // 2. Not enough results. + resp = {{}}; + AvalancheTest::runEventLoop(p); + BOOST_CHECK(!p.registerVotes(avanode->GetId(), resp, accepted, rejected)); + BOOST_CHECK_EQUAL(accepted.size(), 0); + BOOST_CHECK_EQUAL(rejected.size(), 0); + BOOST_CHECK_EQUAL(AvalancheTest::getSuitableNodeToQuery(p), avanodeid); + + // 3. Do not match the poll. + resp = {{AvalancheVote()}}; + AvalancheTest::runEventLoop(p); + BOOST_CHECK(!p.registerVotes(avanode->GetId(), resp, accepted, rejected)); + BOOST_CHECK_EQUAL(accepted.size(), 0); + BOOST_CHECK_EQUAL(rejected.size(), 0); + BOOST_CHECK_EQUAL(AvalancheTest::getSuitableNodeToQuery(p), avanodeid); + + // Proper response gets processed and avanode is available again. + resp = {{AvalancheVote(0, blockHash)}}; + AvalancheTest::runEventLoop(p); + BOOST_CHECK(p.registerVotes(avanode->GetId(), resp, accepted, rejected)); + BOOST_CHECK_EQUAL(accepted.size(), 0); + BOOST_CHECK_EQUAL(rejected.size(), 0); + BOOST_CHECK_EQUAL(AvalancheTest::getSuitableNodeToQuery(p), avanodeid); + + // Making request for invalid nodes do not work. + BOOST_CHECK( + !p.registerVotes(avanode->GetId() + 1234, resp, accepted, rejected)); + BOOST_CHECK_EQUAL(accepted.size(), 0); + BOOST_CHECK_EQUAL(rejected.size(), 0); + + CConnmanTest::ClearNodes(); } BOOST_AUTO_TEST_CASE(event_loop) { - AvalancheProcessor p; - CScheduler scheduler; + AvalancheProcessor p(g_connman.get()); + CScheduler s; // Starting the event loop. - BOOST_CHECK(p.startEventLoop(scheduler)); + BOOST_CHECK(p.startEventLoop(s)); // There is one task planned in the next hour (our event loop). boost::chrono::system_clock::time_point start, stop; - BOOST_CHECK_EQUAL(scheduler.getQueueInfo(start, stop), 1); + BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 1); // Starting twice doesn't start it twice. - BOOST_CHECK(!p.startEventLoop(scheduler)); + BOOST_CHECK(!p.startEventLoop(s)); // Start the scheduler thread. - std::thread schedulerThread( - std::bind(&CScheduler::serviceQueue, &scheduler)); + std::thread schedulerThread(std::bind(&CScheduler::serviceQueue, &s)); + + // Create a node and a block to query. + const Config &config = GetConfig(); + CBlockIndex index; + + // Create a node that supports avalanche. + auto avanode = ConnectNode(config, NODE_AVALANCHE, *peerLogic); + NodeId nodeid = avanode->GetId(); + + // Make sure the block has a hash. + static const uint256 blockHash(uint256S( + "abcdef0000000000000000000000000000000000000000000000000000000001")); + index.phashBlock = &blockHash; + + // There is no query in flight at the moment. + BOOST_CHECK_EQUAL(AvalancheTest::getSuitableNodeToQuery(p), nodeid); + + // Add a new block. Check it is added to the polls. + BOOST_CHECK(p.addBlockToReconcile(&index)); + + bool hasQueried = false; + for (int i = 0; i < 1000; i++) { + // Technically, this is a race condition, but this should do just fine + // as we wait up to 1s for an event that should take 10ms. + boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); + if (AvalancheTest::getSuitableNodeToQuery(p) == -1) { + hasQueried = true; + break; + } + } + + BOOST_CHECK(hasQueried); // Stop event loop. BOOST_CHECK(p.stopEventLoop()); // We don't have any task scheduled anymore. - BOOST_CHECK_EQUAL(scheduler.getQueueInfo(start, stop), 0); + BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 0); // Can't stop the event loop twice. BOOST_CHECK(!p.stopEventLoop()); // Wait for the scheduler to stop. - scheduler.stop(true); + s.stop(true); schedulerThread.join(); + + CConnmanTest::ClearNodes(); } BOOST_AUTO_TEST_CASE(destructor) { - CScheduler scheduler; + CScheduler s; boost::chrono::system_clock::time_point start, stop; // Start the scheduler thread. - std::thread schedulerThread( - std::bind(&CScheduler::serviceQueue, &scheduler)); + std::thread schedulerThread(std::bind(&CScheduler::serviceQueue, &s)); { - AvalancheProcessor p; - BOOST_CHECK(p.startEventLoop(scheduler)); - BOOST_CHECK_EQUAL(scheduler.getQueueInfo(start, stop), 1); + AvalancheProcessor p(g_connman.get()); + BOOST_CHECK(p.startEventLoop(s)); + BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 1); } - // Now that avalanche is destroyed, there is scheduled task anymore. - BOOST_CHECK_EQUAL(scheduler.getQueueInfo(start, stop), 0); + // We don't have any task scheduled anymore that avalanche is destroyed. + BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 0); // Wait for the scheduler to stop. - scheduler.stop(true); + s.stop(true); schedulerThread.join(); }