// Copyright (c) 2011-2016 The Cryptonote developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "BaseFunctionalTests.h" #include #include #include #include #include #include #include #include #include #include #include #include "P2p/NetNodeConfig.h" #include "CryptoNoteCore/CoreConfig.h" #include "CryptoNoteCore/CryptoNoteTools.h" #include "WalletLegacy/WalletLegacy.h" #include "Logger.h" #include "InProcTestNode.h" #include "RPCTestNode.h" #if defined __linux__ #include #include #include #endif #ifdef _WIN32 const std::string DAEMON_FILENAME = std::string(CRYPTONOTE_NAME) + "d.exe"; #else const std::string DAEMON_FILENAME = std::string(CRYPTONOTE_NAME) + "d"; #endif using namespace Tests::Common; using namespace Tests; void BaseFunctionalTests::launchTestnet(size_t count, Topology t) { if (count < 1) { LOG_WARNING("Testnet has no nodes"); } m_testnetSize = count; m_topology = t; nodeDaemons.resize(m_testnetSize); for (size_t i = 0; i < m_testnetSize; ++i) { startNode(i); } waitDaemonsReady(); nodeDaemons[0]->makeINode(mainNode); makeWallet(workingWallet, mainNode); } void BaseFunctionalTests::launchInprocTestnet(size_t count, Topology t) { m_testnetSize = count; m_topology = t; for (size_t i = 0; i < m_testnetSize; ++i) { auto cfg = createNodeConfiguration(i); nodeDaemons.emplace_back(new InProcTestNode(cfg, m_currency)); } waitDaemonsReady(); nodeDaemons[0]->makeINode(mainNode); makeWallet(workingWallet, mainNode); } void BaseFunctionalTests::launchTestnetWithInprocNode(size_t count, Topology t) { if (count < 1) { LOG_WARNING("Testnet has no nodes"); } m_testnetSize = count; m_topology = t; nodeDaemons.resize(m_testnetSize); for (size_t i = 0; i < m_testnetSize - 1; ++i) { startNode(i); } auto cfg = createNodeConfiguration(m_testnetSize - 1); nodeDaemons[m_testnetSize - 1].reset(new InProcTestNode(cfg, m_currency)); waitDaemonsReady(); nodeDaemons[0]->makeINode(mainNode); makeWallet(workingWallet, mainNode); } Tests::TestNodeConfiguration BaseFunctionalTests::createNodeConfiguration(size_t index) { Tests::TestNodeConfiguration cfg; std::string dataDirPath = m_dataDir + "/node" + std::to_string(index); boost::filesystem::create_directory(dataDirPath); cfg.dataDir = dataDirPath; uint16_t rpcPort = static_cast(RPC_FIRST_PORT + index); uint16_t p2pPort = static_cast(P2P_FIRST_PORT + index); cfg.p2pPort = p2pPort; cfg.rpcPort = rpcPort; switch (m_topology) { case Line: if (index != 0) { cfg.exclusiveNodes.push_back("127.0.0.1:" + std::to_string(p2pPort - 1)); } break; case Ring: { uint16_t p2pExternalPort = static_cast(P2P_FIRST_PORT + (index + 1) % m_testnetSize); cfg.exclusiveNodes.push_back("127.0.0.1:" + std::to_string(p2pExternalPort + 1)); break; } case Star: if (index == 0) { for (size_t node = 1; node < m_testnetSize; ++node) { cfg.exclusiveNodes.push_back("127.0.0.1:" + std::to_string(P2P_FIRST_PORT + node)); } } break; } return cfg; } void BaseFunctionalTests::startNode(size_t index) { std::string dataDirPath = m_dataDir + "/node" + std::to_string(index); boost::filesystem::create_directory(dataDirPath); std::ofstream config(dataDirPath + "/daemon.conf", std::ios_base::trunc | std::ios_base::out); uint16_t rpcPort = static_cast(RPC_FIRST_PORT + index); uint16_t p2pPort = static_cast(P2P_FIRST_PORT + index); config << "rpc-bind-port=" << rpcPort << std::endl << "p2p-bind-port=" << p2pPort << std::endl << "log-level=4" << std::endl << "log-file=test_" << CRYPTONOTE_NAME << "d_" << index << ".log" << std::endl; switch (m_topology) { case Line: if (index != 0) { config << "add-exclusive-node=127.0.0.1:" << p2pPort - 1 << std::endl; } break; case Ring: { uint16_t p2pExternalPort = static_cast(P2P_FIRST_PORT + (index + 1) % m_testnetSize); config << "add-exclusive-node=127.0.0.1:" << (p2pExternalPort + 1) << std::endl; break; } case Star: if (index == 0) { for (size_t node = 1; node < m_testnetSize; ++node) { config << "add-exclusive-node=127.0.0.1:" << (P2P_FIRST_PORT + node) << std::endl; } } break; } config.close(); boost::filesystem::path daemonPath = index < m_config.daemons.size() ? boost::filesystem::path(m_config.daemons[index]) : (boost::filesystem::path(m_daemonDir) / DAEMON_FILENAME); boost::system::error_code ignoredEc; if (!boost::filesystem::exists(daemonPath, ignoredEc)) { throw std::runtime_error("daemon binary wasn't found"); } #if defined WIN32 std::string commandLine = "start /MIN \"" + std::string(CRYPTONOTE_NAME) + "d" + std::to_string(index) + "\" \"" + daemonPath.string() + "\" --testnet --data-dir=\"" + dataDirPath + "\" --config-file=daemon.conf"; LOG_DEBUG(commandLine); system(commandLine.c_str()); #elif defined __linux__ auto pid = fork(); if (pid == 0) { std::string pathToDaemon = daemonPath.string(); close(1); close(2); std::string dataDir = "--data-dir=" + dataDirPath + ""; LOG_TRACE(pathToDaemon); if (execl(pathToDaemon.c_str(), (std::string(CRYPTONOTE_NAME) + "d").c_str(), "--testnet", dataDir.c_str(), "--config-file=daemon.conf", NULL) == -1) { LOG_ERROR(TO_STRING(errno)); } abort(); // throw std::runtime_error("failed to start daemon"); } else if (pid > 0) { pids.resize(m_testnetSize, 0); assert(pids[index] == 0); pids[index] = pid; } #else assert(false); #endif assert(nodeDaemons.size() > index); nodeDaemons[index] = std::unique_ptr(new RPCTestNode(rpcPort, m_dispatcher)); } void BaseFunctionalTests::stopNode(size_t index) { assert(nodeDaemons[index].get() != nullptr); bool ok = nodeDaemons[index]->stopDaemon(); assert(ok); std::this_thread::sleep_for(std::chrono::milliseconds(5000)); nodeDaemons[index].release(); #ifdef __linux__ int status; assert(pids[index] != 0); while (-1 == waitpid(pids[index], &status, 0)); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { std::cerr << "Process " << " (pid " << pids[index] << ") failed" << std::endl; exit(1); } pids[index] = 0; #endif } bool BaseFunctionalTests::waitDaemonsReady() { for (size_t i = 0; i < nodeDaemons.size(); ++i) { bool ok = waitDaemonReady(i); if (!ok) { return false; } } return true; } bool BaseFunctionalTests::waitDaemonReady(size_t nodeIndex) { assert(nodeIndex < nodeDaemons.size() && nodeDaemons[nodeIndex].get() != nullptr); for (size_t i = 0; ; ++i) { if (nodeDaemons[nodeIndex]->getLocalHeight() > 0) { break; } else if (i < 2 * 60) { std::this_thread::sleep_for(std::chrono::seconds(1)); } else { return false; } } return true; } BaseFunctionalTests::~BaseFunctionalTests() { if (mainNode) { mainNode->shutdown(); } stopTestnet(); for (size_t i = 0; i < m_testnetSize; ++i) { boost::system::error_code ignoredErrorCode; auto nodeDataDir = boost::filesystem::path(m_dataDir) / boost::filesystem::path("node" + std::to_string(i)); boost::filesystem::remove_all(nodeDataDir, ignoredErrorCode); } } namespace { class WaitForCoinBaseObserver : public CryptoNote::IWalletLegacyObserver { Semaphore& m_gotReward; CryptoNote::IWalletLegacy& m_wallet; public: WaitForCoinBaseObserver(Semaphore& gotReward, CryptoNote::IWalletLegacy& wallet) : m_gotReward(gotReward), m_wallet(wallet) { } virtual void externalTransactionCreated(CryptoNote::TransactionId transactionId) override { CryptoNote::WalletLegacyTransaction trInfo; m_wallet.getTransaction(transactionId, trInfo); if (trInfo.isCoinbase) m_gotReward.notify(); } }; } bool BaseFunctionalTests::mineBlocks(TestNode& node, const CryptoNote::AccountPublicAddress& address, size_t blockCount) { for (size_t i = 0; i < blockCount; ++i) { Block blockTemplate; uint64_t difficulty; if (!node.getBlockTemplate(m_currency.accountAddressAsString(address), blockTemplate, difficulty)) { return false; } if (difficulty != 1) { return false; } if (!prepareAndSubmitBlock(node, std::move(blockTemplate))) { return false; } } return true; } bool BaseFunctionalTests::prepareAndSubmitBlock(TestNode& node, CryptoNote::Block&& blockTemplate) { blockTemplate.timestamp = m_nextTimestamp; m_nextTimestamp += 2 * m_currency.difficultyTarget(); BinaryArray blockBlob = CryptoNote::toBinaryArray(blockTemplate); return node.submitBlock(::Common::toHex(blockBlob.data(), blockBlob.size())); } bool BaseFunctionalTests::mineBlock(std::unique_ptr &wallet) { if (nodeDaemons.empty() || !wallet) return false; if (!nodeDaemons.front()->stopMining()) return false; std::this_thread::sleep_for(std::chrono::milliseconds(10000)); Semaphore gotReward; WaitForCoinBaseObserver cbo(gotReward, *wallet.get()); wallet->addObserver(&cbo); if (!nodeDaemons.front()->startMining(1, wallet->getAddress())) return false; gotReward.wait(); if (!nodeDaemons.front()->stopMining()) return false; wallet->removeObserver(&cbo); return true; } bool BaseFunctionalTests::mineBlock() { return mineBlock(workingWallet); } bool BaseFunctionalTests::startMining(size_t threads) { if (nodeDaemons.empty() || !workingWallet) return false; if(!stopMining()) return false; return nodeDaemons.front()->startMining(threads, workingWallet->getAddress()); } bool BaseFunctionalTests::stopMining() { if (nodeDaemons.empty()) return false; return nodeDaemons.front()->stopMining(); } bool BaseFunctionalTests::makeWallet(std::unique_ptr & wallet, std::unique_ptr& node, const std::string& password) { if (!node) return false; wallet = std::unique_ptr(new CryptoNote::WalletLegacy(m_currency, *node)); wallet->initAndGenerate(password); return true; } void BaseFunctionalTests::stopTestnet() { if (nodeDaemons.empty()) { return; } // WORKAROUND: Make sure all contexts, that use daemons, are finished before these daemons will be destroyed // TODO: There is should be used context groups m_dispatcher.yield(); for (auto& daemon : nodeDaemons) { if (daemon) { daemon->stopDaemon(); } } // std::this_thread::sleep_for(std::chrono::milliseconds(5000)); nodeDaemons.clear(); #ifdef __linux__ for (auto& pid : pids) { if (pid != 0) { int status; while (-1 == waitpid(pid, &status, 0)); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { std::cerr << "Process " << " (pid " << pid << ") failed" << std::endl; exit(1); } } } pids.clear(); #endif } namespace { struct PeerCountWaiter : CryptoNote::INodeObserver { System::Dispatcher& m_dispatcher; System::Event m_event; System::Timer m_timer; bool m_timedout = false; bool m_waiting = false; size_t m_expectedPeerCount; PeerCountWaiter(System::Dispatcher& dispatcher) : m_dispatcher(dispatcher), m_event(m_dispatcher), m_timer(m_dispatcher) { } void wait(size_t expectedPeerCount) { m_waiting = true; m_expectedPeerCount = expectedPeerCount; System::ContextGroup cg(m_dispatcher); cg.spawn([&] { try { System::Timer(m_dispatcher).sleep(std::chrono::minutes(2)); m_timedout = true; m_event.set(); } catch (System::InterruptedException&) { } }); cg.spawn([&] { m_event.wait(); cg.interrupt(); }); cg.wait(); m_waiting = false; } virtual void peerCountUpdated(size_t count) override { m_dispatcher.remoteSpawn([this, count]() { if (m_waiting && count == m_expectedPeerCount) { m_event.set(); } }); } }; } bool BaseFunctionalTests::waitForPeerCount(CryptoNote::INode& node, size_t expectedPeerCount) { PeerCountWaiter peerCountWaiter(m_dispatcher); node.addObserver(&peerCountWaiter); if (node.getPeerCount() != expectedPeerCount) { peerCountWaiter.wait(expectedPeerCount); } node.removeObserver(&peerCountWaiter); // TODO workaround: make sure ObserverManager doesn't have local pointers to peerCountWaiter, so it can be destroyed std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Run all spawned handlers from PeerCountWaiter::peerCountUpdated m_dispatcher.yield(); return !peerCountWaiter.m_timedout; } namespace { struct PoolUpdateWaiter : public INodeObserver { System::Dispatcher& m_dispatcher; System::Event& m_event; PoolUpdateWaiter(System::Dispatcher& dispatcher, System::Event& event) : m_dispatcher(dispatcher), m_event(event) { } virtual void poolChanged() override { m_dispatcher.remoteSpawn([this]() { m_event.set(); }); } }; } bool BaseFunctionalTests::waitForPoolSize(size_t nodeIndex, CryptoNote::INode& node, size_t expectedPoolSize, std::vector>& txPool) { System::Event event(m_dispatcher); PoolUpdateWaiter poolUpdateWaiter(m_dispatcher, event); node.addObserver(&poolUpdateWaiter); bool ok; for (size_t i = 0; ; ++i) { ok = getNodeTransactionPool(nodeIndex, node, txPool); if (!ok) { break; } if (txPool.size() == expectedPoolSize) { break; } // TODO NodeRpcProxy doesn't send poolChanged() notification!!! //event.wait(); //event.clear(); // WORKAROUND if (i < 3 * P2P_DEFAULT_HANDSHAKE_INTERVAL) { std::this_thread::sleep_for(std::chrono::seconds(1)); } else { ok = false; break; } } node.removeObserver(&poolUpdateWaiter); // TODO workaround: make sure ObserverManager doesn't have local pointers to poolUpdateWaiter, so it can be destroyed std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Run all spawned handlers from PoolUpdateWaiter::poolChanged m_dispatcher.yield(); return ok; } bool BaseFunctionalTests::getNodeTransactionPool(size_t nodeIndex, CryptoNote::INode& node, std::vector>& txPool) { assert(nodeIndex < nodeDaemons.size() && nodeDaemons[nodeIndex].get() != nullptr); auto& daemon = *nodeDaemons[nodeIndex]; Crypto::Hash tailBlockId; bool updateTailBlockId = true; while (true) { if (updateTailBlockId) { if (!daemon.getTailBlockId(tailBlockId)) { return false; } updateTailBlockId = false; } System::Event poolReceivedEvent(m_dispatcher); std::error_code ec; bool isTailBlockActual; std::vector> addedTxs; std::vector deletedTxsIds; node.getPoolSymmetricDifference(std::vector(), tailBlockId, isTailBlockActual, addedTxs, deletedTxsIds, [this, &poolReceivedEvent, &ec](std::error_code result) { ec = result; m_dispatcher.remoteSpawn([&poolReceivedEvent]() { poolReceivedEvent.set(); }); } ); poolReceivedEvent.wait(); if (ec) { return false; } else if (!isTailBlockActual) { updateTailBlockId = true; } else { txPool = std::move(addedTxs); break; } } return true; }