// Copyright (c) 2012-2015, The CryptoNote developers, The Bytecoin developers
//
// This file is part of Bytecoin.
//
// Bytecoin is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Bytecoin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Bytecoin. If not, see .
#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 = "bytecoind.exe";
#else
const std::string DAEMON_FILENAME = "bytecoind";
#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_bytecoind_" << 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 \"bytecoind" + 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(), "bytecoind", "--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();
if (blockTemplate.majorVersion == BLOCK_MAJOR_VERSION_2) {
blockTemplate.parentBlock.majorVersion = BLOCK_MAJOR_VERSION_1;
blockTemplate.parentBlock.minorVersion = BLOCK_MINOR_VERSION_0;
blockTemplate.parentBlock.transactionCount = 1;
CryptoNote::TransactionExtraMergeMiningTag mmTag;
mmTag.depth = 0;
if (!CryptoNote::get_aux_block_header_hash(blockTemplate, mmTag.merkleRoot)) {
return false;
}
blockTemplate.parentBlock.baseTransaction.extra.clear();
if (!CryptoNote::appendMergeMiningTagToExtra(blockTemplate.parentBlock.baseTransaction.extra, mmTag)) {
return false;
}
}
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;
}