// SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2020, The Monero Project. #include #include #include #include #include "nodes.h" #include "utils/utils.h" #include "utils/networking.h" #include "appcontext.h" Nodes::Nodes(AppContext *ctx, QNetworkAccessManager *networkAccessManager, QObject *parent) : QObject(parent), m_ctx(ctx), m_networkAccessManager(networkAccessManager), m_connection(FeatherNode()), modelWebsocket(new NodeModel(NodeSource::websocket, this)), modelCustom(new NodeModel(NodeSource::custom, this)) { this->loadConfig(); connect(m_connectionTimer, &QTimer::timeout, this, &Nodes::onConnectionTimer); } void Nodes::loadConfig() { QString msg; auto configNodes = config()->get(Config::nodes).toByteArray(); auto key = QString::number(m_ctx->networkType); if (!Utils::validateJSON(configNodes)) { m_configJson[key] = QJsonObject(); qCritical() << "fixed malformed config key \"nodes\""; } QJsonDocument doc = QJsonDocument::fromJson(configNodes); m_configJson = doc.object(); if(!m_configJson.contains(key)) m_configJson[key] = QJsonObject(); auto obj = m_configJson.value(key).toObject(); if (!obj.contains("custom")) obj["custom"] = QJsonArray(); if (!obj.contains("ws")) obj["ws"] = QJsonArray(); // load custom nodes auto nodes = obj.value("custom").toArray(); foreach (const QJsonValue &value, nodes) { auto customNode = FeatherNode(value.toString()); customNode.custom = true; if(m_connection == customNode) { if(m_connection.isActive) customNode.isActive = true; else if(m_connection.isConnecting) customNode.isConnecting = true; } m_customNodes.append(customNode); } // load cached websocket nodes auto ws = obj.value("ws").toArray(); foreach (const QJsonValue &value, ws) { auto wsNode = FeatherNode(value.toString()); wsNode.custom = false; wsNode.online = true; // assume online if(m_connection == wsNode) { if(m_connection.isActive) wsNode.isActive = true; else if(m_connection.isConnecting) wsNode.isConnecting = true; } m_websocketNodes.append(wsNode); } if (!obj.contains("source")) obj["source"] = NodeSource::websocket; m_source = static_cast(obj.value("source").toInt()); if(m_websocketNodes.count() > 0){ msg = QString("Loaded %1 cached websocket nodes from config").arg(m_websocketNodes.count()); activityLog.append(msg); } if(m_customNodes.count() > 0){ msg = QString("Loaded %1 custom nodes from config").arg(m_customNodes.count()); activityLog.append(msg); } m_configJson[key] = obj; this->writeConfig(); this->updateModels(); } void Nodes::writeConfig() { QJsonDocument doc(m_configJson); QString output(doc.toJson(QJsonDocument::Compact)); config()->set(Config::nodes, output); auto msg = QString("Saved node config."); activityLog.append(msg); } void Nodes::connectToNode() { // auto connect m_connectionAttempts.clear(); m_wsExhaustedWarningEmitted = false; m_customExhaustedWarningEmitted = false; m_connectionTimer->start(2000); this->onConnectionTimer(); } void Nodes::connectToNode(FeatherNode node) { if(node.address.isEmpty()) return; emit updateStatus(QString("Connecting to %1").arg(node.address)); auto msg = QString("Attempting to connect to %1 (%2)") .arg(node.address).arg(node.custom ? "custom" : "ws"); qInfo() << msg; activityLog.append(msg); if (!node.username.isEmpty() && !node.password.isEmpty()) m_ctx->currentWallet->setDaemonLogin(node.username, node.password); m_ctx->currentWallet->initAsync(node.address, true, 0, false, false, 0); m_connectionAttemptTime = std::time(nullptr); m_connection = node; m_connection.isActive = false; m_connection.isConnecting = true; this->resetLocalState(); this->updateModels(); m_connectionTimer->start(1000); } void Nodes::onConnectionTimer() { // this function is responsible for automatically connecting to a daemon. if (m_ctx->currentWallet == nullptr) { m_connectionTimer->stop(); return; } QString msg; Wallet::ConnectionStatus status = m_ctx->currentWallet->connected(true); NodeSource nodeSource = this->source(); auto wsMode = (nodeSource == NodeSource::websocket); auto nodes = wsMode ? m_customNodes : m_websocketNodes; if (wsMode && !m_wsNodesReceived && m_websocketNodes.count() == 0) { // this situation should rarely occur due to the usage of the websocket node cache on startup. msg = QString("Feather is in websocket connection mode but was not able to receive any nodes (yet)."); qInfo() << msg; activityLog.append(msg); return; } if (status == Wallet::ConnectionStatus::ConnectionStatus_Disconnected) { // try a connect auto node = this->pickEligibleNode(); this->connectToNode(node); return; } else if (status == Wallet::ConnectionStatus::ConnectionStatus_Connecting){ if (!m_connection.isConnecting) { // Weirdly enough, status == connecting directly after a wallet is opened. auto node = this->pickEligibleNode(); this->connectToNode(node); return; } // determine timeout unsigned int nodeConnectionTimeout = 6; if(m_connection.tor) nodeConnectionTimeout = 25; auto connectionTimeout = static_cast(std::time(nullptr) - m_connectionAttemptTime); if(connectionTimeout < nodeConnectionTimeout) return; // timeout not reached yet msg = QString("Node connection attempt stale after %1 seconds, picking new node").arg(nodeConnectionTimeout); activityLog.append(msg); qInfo() << msg; auto newNode = this->pickEligibleNode(); this->connectToNode(newNode); return; } else if(status == Wallet::ConnectionStatus::ConnectionStatus_Connected) { // wallet is connected to daemon successfully, poll status every 3 seconds if(!m_connection.isConnecting) return; msg = QString("Node connected to %1").arg(m_connection.address); qInfo() << msg; activityLog.append(msg); // set current connection object m_connection.isConnecting = false; m_connection.isActive = true; this->resetLocalState(); this->updateModels(); // reset node exhaustion state m_connectionAttempts.clear(); m_wsExhaustedWarningEmitted = false; m_customExhaustedWarningEmitted = false; m_connectionTimer->setInterval(3000); } } FeatherNode Nodes::pickEligibleNode() { // Pick a node at random to connect to auto rtn = FeatherNode(); NodeSource nodeSource = this->source(); auto wsMode = nodeSource == NodeSource::websocket; auto nodes = wsMode ? m_websocketNodes : m_customNodes; if (nodes.count() == 0) { this->exhausted(); return rtn; } QVector heights; for (const auto &node: nodes) { heights.push_back(node.height); } std::sort(heights.begin(), heights.end()); // Calculate mode of node heights int max_count = 1, mode_height = heights[0], count = 1; for (int i = 1; i < heights.count(); i++) { if (heights[i] == 0) { // Don't consider 0 height nodes continue; } if (heights[i] == heights[i - 1]) count++; else { if (count > max_count) { max_count = count; mode_height = heights[i - 1]; } count = 1; } } if (count > max_count) { max_count = count; mode_height = heights[heights.count() - 1]; } while(true) { // keep track of nodes we have previously tried to connect to if (m_connectionAttempts.count() == nodes.count()) { this->exhausted(); m_connectionTimer->stop(); return rtn; } int random = QRandomGenerator::global()->bounded(nodes.count()); FeatherNode node = nodes.at(random); if (m_connectionAttempts.contains(node.full)) continue; m_connectionAttempts.append(node.full); if (wsMode) { // Ignore offline nodes if (!node.online) continue; // Ignore nodes that are more than 25 blocks behind mode if (node.height < (mode_height - 25)) continue; // Ignore nodes that say they aren't synchronized if (node.target_height > node.height) continue; } return node; } } void Nodes::onWSNodesReceived(const QList> &nodes) { m_websocketNodes.clear(); m_wsNodesReceived = true; for(auto &node: nodes) { if(m_connection == *node) { if(m_connection.isActive) node->isActive = true; else if(m_connection.isConnecting) node->isConnecting = true; } m_websocketNodes.push_back(*node); } // cache into config auto key = QString::number(m_ctx->networkType); auto obj = m_configJson.value(key).toObject(); auto ws = QJsonArray(); for(auto const &node: m_websocketNodes) ws.push_back(node.address); obj["ws"] = ws; m_configJson[key] = obj; this->writeConfig(); this->resetLocalState(); this->updateModels(); } void Nodes::onNodeSourceChanged(NodeSource nodeSource) { if(nodeSource == this->source()) return; auto key = QString::number(m_ctx->networkType); auto obj = m_configJson.value(key).toObject(); obj["source"] = nodeSource; m_configJson[key] = obj; this->writeConfig(); this->resetLocalState(); this->updateModels(); } void Nodes::setCustomNodes(QList nodes) { m_customNodes.clear(); auto key = QString::number(m_ctx->networkType); auto obj = m_configJson.value(key).toObject(); QStringList nodesList; for(auto const &node: nodes) { if(nodesList.contains(node.full)) continue; nodesList.append(node.full); m_customNodes.append(node); } auto arr = QJsonArray::fromStringList(nodesList); obj["custom"] = arr; m_configJson[key] = obj; this->writeConfig(); this->resetLocalState(); this->updateModels(); } void Nodes::updateModels() { this->modelCustom->updateNodes(m_customNodes); this->modelWebsocket->updateNodes(m_websocketNodes); } void Nodes::resetLocalState() { QList*> models = {&m_customNodes, &m_websocketNodes}; for(QList *model: models) { for (FeatherNode &_node: *model) { _node.isConnecting = false; _node.isActive = false; if (_node == m_connection) { _node.isActive = m_connection.isActive; _node.isConnecting = m_connection.isConnecting; } } } } void Nodes::exhausted() { NodeSource nodeSource = this->source(); auto wsMode = nodeSource == NodeSource::websocket; if(wsMode) this->WSNodeExhaustedWarning(); else this->nodeExhaustedWarning(); } void Nodes::nodeExhaustedWarning(){ if(m_customExhaustedWarningEmitted) return; emit nodeExhausted(); auto msg = QString("Could not find an eligible custom node to connect to."); qWarning() << msg; activityLog.append(msg); m_customExhaustedWarningEmitted = true; this->m_connectionTimer->stop(); } void Nodes::WSNodeExhaustedWarning() { if(m_wsExhaustedWarningEmitted) return; emit WSNodeExhausted(); auto msg = QString("Could not find an eligible websocket node to connect to."); qWarning() << msg; activityLog.append(msg); m_wsExhaustedWarningEmitted = true; this->m_connectionTimer->stop(); } void Nodes::onWalletClosing() { m_connectionTimer->stop(); } QList Nodes::customNodes() { return m_customNodes; } FeatherNode Nodes::connection() { return m_connection; } void Nodes::stopTimer(){ m_connectionTimer->stop(); } NodeSource Nodes::source() { return m_source; }