// // Fulcrum - A fast & nimble SPV Server for Bitcoin Cash // Copyright (C) 2019-2020 Calin A. Culianu // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program 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 General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program (see LICENSE.txt). If not, see // . // #include #include "bitcoin/rpc/protocol.h" #include "BitcoinD.h" BitcoinDMgr::BitcoinDMgr(const QHostAddress &host, quint16 port, const QString &user, const QString &pass) : Mgr(nullptr), IdMixin(newId()), host(host), port(port), user(user), pass(pass) { setObjectName("BitcoinDMgr"); _thread.setObjectName(objectName()); } BitcoinDMgr::~BitcoinDMgr() { cleanup(); } void BitcoinDMgr::startup() { Log() << objectName() << ": starting " << N_CLIENTS << " " << Util::Pluralize("bitcoin rpc client", N_CLIENTS) << " ..."; for (auto & client : clients) { client = std::make_unique(host, port, user, pass); // connect client to us -- TODO: figure out workflow: how requests for work and results will get dispatched connect(client.get(), &BitcoinD::gotMessage, this, &BitcoinDMgr::on_Message); connect(client.get(), &BitcoinD::gotErrorMessage, this, &BitcoinDMgr::on_ErrorMessage); connect(client.get(), &BitcoinD::authenticated, this, [this](BitcoinD *b){ // guard against stale/old signal if (!b->isGood()) { Debug() << "got authenticated for id:" << b->id << " but isGood() is false!"; return; // false/stale signal } const bool wasEmpty = goodSet.empty(); goodSet.insert(b->id); if (wasEmpty) emit gotFirstGoodConnection(b->id); }); connect(client.get(), &BitcoinD::lostConnection, this, [this](AbstractConnection *c){ // guard against stale/old signal if (c->isGood()) { Debug() << "got lostConnection for id:" << c->id << " but isGood() is true!"; return; // false/stale signal } goodSet.erase(c->id); auto constexpr chkTimer = "checkNoMoreBitcoinDs"; // we throttle the spamming of the allConnectionsLost signal via this mechanism callOnTimerSoonNoRepeat(miniTimeout, chkTimer, [this]{ if (goodSet.empty()) emit allConnectionsLost(); }, true); }); client->start(); } start(); Log() << objectName() << ": started ok"; } void BitcoinDMgr::cleanup() { stop(); for (auto & client : clients) { client.reset(); /// implicitly calls client->stop() } goodSet.clear(); Debug() << "BitcoinDMgr cleaned up"; } void BitcoinDMgr::on_Message(quint64 bid, const RPC::Message &msg) { if (Trace::isEnabled()) Trace() << "Msg from: " << bid << " method=" << msg.method; } void BitcoinDMgr::on_ErrorMessage(quint64 bid, const RPC::Message &msg) { Debug() << "ErrMsg from: " << bid << " error=" << msg.errorMessage(); if (msg.errorCode() == bitcoin::RPCErrorCode::RPC_IN_WARMUP) { emit inWarmUp(msg.errorMessage()); } } auto BitcoinDMgr::stats() const -> Stats { QVariantList l; constexpr int timeout = kDefaultTimeout/qMax(N_CLIENTS,1); for (const auto & client : clients) { if (!client) continue; auto map = client->statsSafe(timeout).toMap(); auto name = map.take("name").toString(); l += QVariantMap({{ name, map }}); } QVariantMap m; m["rpc clients"] = l; m["extant request contexts"] = BitcoinDMgrHelper::ReqCtxObj::extant.load(); return m; } BitcoinD *BitcoinDMgr::getBitcoinD() { BitcoinD *ret = nullptr; unsigned which = 0; if (unsigned n = quint32(goodSet.size()); n > 1) which = QRandomGenerator::system()->bounded(n); // pick a random client in the set (which is an index of the "good clients") if (!goodSet.empty()) { // linear search for a bitcoind that is not lastBitcoinDUsed unsigned i = 0; for (auto & client : clients) { if (goodSet.count(client->id) && i++ == which && client->isGood()) { ret = client.get(); break; } } } return ret; } /// this is safe to call from any thread. internally it dispatches messages to this obejct's thread. /// does not throw. Results/Error/Fail functions are called in the context of the sender's thread. /// Returns the BitcoinD->id that was given the message. void BitcoinDMgr::submitRequest(QObject *sender, const RPC::Message::Id &rid, const QString & method, const QVariantList & params, const ResultsF & resf, const ErrorF & errf, const FailF & failf) { using namespace BitcoinDMgrHelper; constexpr bool debugDeletes = false; // set this to true to print debug messages tracking all the below object deletions (tested: no leaks!) // A note about ownership: this context object is owned by the connections below both to ->sender and from bitcoind // ->context. It will be auto-deleted when the shared_ptr refct held by the lambdas drops to 0. This is guaranteed // to happen either as a result of a successful request reply, or due to bitcoind failure. auto context = std::shared_ptr(new ReqCtxObj, [](ReqCtxObj *context){ if constexpr (debugDeletes) { Debug() << context->objectName() << " shptr deleter"; connect(context, &QObject::destroyed, qApp, [n=context->objectName()]{ Debug() << n << " destroyed"; }, Qt::DirectConnection); } context->deleteLater(); }); context->setObjectName(QString("context for '%1' request id: %2").arg(sender ? sender->objectName() : "").arg(rid.toString())); connect(context.get(), &ReqCtxObj::results, sender, [context, resf](const RPC::Message &response) { if (!context->replied.exchange(true) && resf) resf(response); context->disconnect(); // kills all lambdas and shared ptr, should cause deleter to execute }); connect(context.get(), &ReqCtxObj::error, sender, [context, errf](const RPC::Message &response) { if (!context->replied.exchange(true) && errf) errf(response); context->disconnect(); // kills all lambdas and shared ptr, should cause deleter to execute }); connect(context.get(), &ReqCtxObj::fail, sender, [context, failf](const RPC::Message::Id &origId, const QString & failureReason) { if (!context->replied.exchange(true) && failf) failf(origId, failureReason); context->disconnect(); // kills all lambdas and shared ptr, should cause deleter to execute }); context->moveToThread(this->thread()); // schedule this ASAP Util::AsyncOnObject(this, [this, context, rid, method, params] { auto bd = getBitcoinD(); if (UNLIKELY(!bd)) { emit context->fail(rid, "Unable to find a good BitcoinD connection"); return; } using ConnsList = decltype (context->conns); static const auto killConns = [](ConnsList & conns) { for (const auto & conn : conns) { QObject::disconnect(conn); } conns.clear(); }; context->conns += connect(bd, &BitcoinD::gotMessage, context.get(), [context, rid](quint64, const RPC::Message &reply){ if (reply.id == rid) {// filter out messages not for us emit context->results(reply); killConns(context->conns); // to kill lambdas, shared ptr captures } }); context->conns += connect(bd, &BitcoinD::gotErrorMessage, context.get(), [context, rid](quint64, const RPC::Message &errMsg){ if (errMsg.id == rid) { // filter out error messages not for us emit context->error(errMsg); killConns(context->conns); // to kill lambdas, shared ptr captures } }); context->conns += connect(bd, &BitcoinD::lostConnection, context.get(), [context, rid](AbstractConnection *){ emit context->fail(rid, "connection lost"); killConns(context->conns); // to kill lambdas, shared ptr captures }); context->conns += connect(bd, &QObject::destroyed, context.get(), [context, rid](QObject *){ emit context->fail(rid, "bitcoind client deleted"); killConns(context->conns); // to kill lambdas, shared ptr captures }); bd->sendRequest(rid, method, params); }); // .. aand.. return right away } namespace BitcoinDMgrHelper { /* static */ std::atomic_int ReqCtxObj::extant{0}; ReqCtxObj::ReqCtxObj() : QObject(nullptr) { ++extant; } ReqCtxObj::~ReqCtxObj() { --extant; } } /* --- BitcoinD --- */ auto BitcoinD::stats() const -> Stats { auto m = RPC::HttpConnection::stats().toMap(); m["lastPeerError"] = badAuth ? "Auth Failure" : lastPeerError; m.remove("nErrorsSent"); // should always be 0 m.remove("nNotificationsSent"); // again, 0 m.remove("nResultsSent"); // again, 0 return m; } BitcoinD::BitcoinD(const QHostAddress &host, quint16 port, const QString & user, const QString &pass, qint64 maxBuffer) : RPC::HttpConnection(RPC::MethodMap{}, newId(), nullptr, maxBuffer), host(host), port(port) { static int N = 1; setObjectName(QString("BitcoinD.%1").arg(N++)); _thread.setObjectName(objectName()); setAuth(user, pass); setV1(true); // bitcoind uses jsonrpc v1 pingtime_ms = 10000; stale_threshold = pingtime_ms * 2; connectMiscSignals(); } BitcoinD::~BitcoinD() { stop(); } void BitcoinD::connectMiscSignals() { connect(this, &BitcoinD::gotMessage, this, [this]{ // this hook emits "authenticated" as soon as we get a good result message via 'do_ping' initiated from 'on_connected' below if (needAuth || badAuth) { needAuth = badAuth = false; emit authenticated(this); } }); } bool BitcoinD::isGood() const { return !badAuth && !needAuth && RPC::HttpConnection::isGood(); } void BitcoinD::on_started() { ThreadObjectMixin::on_started(); { // setup the "reconnect timer" constexpr auto reconnectTimer = "reconnectTimer"; const auto SetTimer = [this] { callOnTimerSoon(5000, reconnectTimer, [this]{ if (!isGood()) { Debug() << prettyName() << " reconnecting..."; reconnect(); return true; // keep the timer alive } return false; // kill timer }); }; conns += connect(this, &BitcoinD::lostConnection, this, [SetTimer]{ Log() << "Lost connection to bitcoind, will retry every 5 seconds ..."; SetTimer(); }); conns += connect(this, &BitcoinD::authFailure, this, [SetTimer, this] { Error() << "Authentication to bitcoind rpc failed. Please check the rpcuser and rpcpass are correct and restart!"; badAuth = true; SetTimer(); }); conns += connect(this, &BitcoinD::authenticated, this, [this] { stopTimer(reconnectTimer); }); SetTimer(); } reconnect(); } void BitcoinD::reconnect() { if (socket) delete socket; socket = new QTcpSocket(this); socketConnectSignals(); socket->connectToHost(host, port); } void BitcoinD::on_connected() { RPC::HttpConnection::on_connected(); lastGood = Util::getTime(); nSent = nReceived = 0; lastPeerError.clear(); lastSocketError.clear(); badAuth = false; needAuth = true; emit connected(this); // note that the 'authenticated' signal is only emitted after good auth is confirmed via the reply from the do_ping below do_ping(); } void BitcoinD::do_ping() { if (isStale()) { Debug() << "Stale connection, reconnecting."; reconnect(); } else emit sendRequest(newId(), "ping"); }