From c7fca3165324e84147985b86efc8424c547397fc Mon Sep 17 00:00:00 2001 From: jwinterm Date: Fri, 4 Sep 2026 05:36:45 -0400 Subject: [PATCH] fix(mining): only offer to mine on a node that is ours MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Start Mining was live regardless of which daemon the wallet was talking to. It is a `start_mining` RPC to the connected node, so on a remote one it is either refused — the button appearing to do nothing — or, if the operator left the RPC open, it sets someone else's machine mining to our address. Neither is a thing a wallet should offer. The axe now greys out unless `Nodes::connection().isLocal()`, hovering it explains why, and clicking anyway flashes the same notice rather than silently doing nothing. `onStartMining` refuses too: the QML disables the control, but a signal is not a promise. isLocal() counts localhost, .local, 127/8 and the RFC1918 ranges, so the embedded node and a daemon on the LAN both qualify. Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF --- src/MainWindow.cpp | 2 +- src/assets/qml/mining.qml | 56 ++++++++++++++++++++++++++++++++++-- src/widgets/MiningWidget.cpp | 11 ++++++- src/widgets/MiningWidget.h | 14 ++++++++- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index dc6ca33c..e1c4e4c5 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -252,7 +252,7 @@ void MainWindow::initWidgets() { }); // [Mining] — wowlet: the pixel-art mining tab (mining.qml + embedded-node solo miner) - m_miningWidget = new MiningWidget(m_wallet, this); + m_miningWidget = new MiningWidget(m_wallet, m_nodes, this); ui->tabWidget->addTab(m_miningWidget, "Mining"); // [Notes] diff --git a/src/assets/qml/mining.qml b/src/assets/qml/mining.qml index 391e0915..e105bcda 100644 --- a/src/assets/qml/mining.qml +++ b/src/assets/qml/mining.qml @@ -512,6 +512,10 @@ Rectangle { Image { id: imgAxe visible: mining.daemonMiningState === 0 + // Greyed out while the wallet is on someone else's node: + // solo mining is an RPC to the daemon we are connected + // to, and that daemon has to be ours. + opacity: mining.canMine ? 1.0 : 0.35 source: "qrc:/mining/axe.png" width: 73 height: 75 @@ -551,7 +555,11 @@ Rectangle { cursorShape: Qt.PointingHandCursor onClicked: { - if(mining.daemonMiningState === 0) { + if (mining.daemonMiningState === 0) { + if (!mining.canMine) { + remoteNodeNotice.flash(); + return; + } root.startMining(); root.calcAvailableHeightConsoleLines(); } @@ -559,14 +567,56 @@ Rectangle { root.stopMining(); } onEntered: { - imgAxe.height = 64 - imgAxe.width = 64 + if (mining.canMine) { + imgAxe.height = 64 + imgAxe.width = 64 + } else { + remoteNodeNotice.show(); + } } onExited: { imgAxe.height = 75 imgAxe.width = 73 + remoteNodeNotice.hideSoon(); } } + + // Why the axe is greyed out. Shown on hover, and flashed if + // the axe is clicked anyway. + Rectangle { + id: remoteNodeNotice + z: parent.z + 5 + visible: opacity > 0.01 + opacity: 0.0 + width: 250 + height: noticeText.implicitHeight + 16 + radius: 6 + color: "#231a10" + border.color: "#e0b062" + border.width: 1 + anchors.right: parent.left + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + + Text { + id: noticeText + anchors.centerIn: parent + width: parent.width - 16 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: "#f2dcb4" + font.pointSize: 9 + text: "Mining needs your own node.\nYou are connected to a remote one — " + + "start the built-in node or point the wallet at a local daemon " + + "under Settings to mine." + } + + Behavior on opacity { NumberAnimation { duration: 160 } } + Timer { id: noticeHide; interval: 2600; onTriggered: remoteNodeNotice.opacity = 0.0 } + function show() { noticeHide.stop(); opacity = 1.0 } + function hideSoon() { noticeHide.restart() } + function flash() { show(); noticeHide.restart() } + } } } } diff --git a/src/widgets/MiningWidget.cpp b/src/widgets/MiningWidget.cpp index 8efa8340..60858ad3 100644 --- a/src/widgets/MiningWidget.cpp +++ b/src/widgets/MiningWidget.cpp @@ -3,6 +3,8 @@ #include "MiningWidget.h" +#include "utils/nodes.h" + #include #include @@ -20,9 +22,10 @@ static QString formatUptime(qint64 secs) { .arg(secs % 60, 2, 10, QChar('0')); } -MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent) +MiningWidget::MiningWidget(Wallet *wallet, Nodes *nodes, QWidget *parent) : QWidget(parent) , m_wallet(wallet) + , m_nodes(nodes) , m_quickWidget(new QQuickWidget(this)) , m_backend(new MiningBackend(this)) , m_timer(new QTimer(this)) @@ -46,6 +49,10 @@ MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent) } void MiningWidget::onStartMining() { + // The QML disables the control, but the signal is not a promise: refuse + // here too rather than send start_mining to a stranger's daemon. + if (!m_backend->canMine()) + return; int threads = std::max(1, QThread::idealThreadCount() / 2); if (m_wallet->startMining(threads)) m_backend->setState(3); @@ -57,6 +64,8 @@ void MiningWidget::onStopMining() { } void MiningWidget::poll() { + m_backend->setCanMine(m_nodes && m_nodes->connection().isLocal()); + bool mining = m_wallet->isMining(); m_backend->setState(mining ? 3 : 0); diff --git a/src/widgets/MiningWidget.h b/src/widgets/MiningWidget.h index 6707019e..1753d734 100644 --- a/src/widgets/MiningWidget.h +++ b/src/widgets/MiningWidget.h @@ -5,6 +5,8 @@ #define WOWLET_MININGWIDGET_H #include + +class Nodes; #include #include @@ -16,16 +18,24 @@ class MiningBackend : public QObject { Q_OBJECT Q_PROPERTY(int daemonMiningState READ daemonMiningState NOTIFY daemonMiningStateChanged) Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged) + Q_PROPERTY(bool canMine READ canMine NOTIFY canMineChanged) public: explicit MiningBackend(QObject *parent = nullptr) : QObject(parent) {} int daemonMiningState() const { return m_state; } void setState(int s) { if (m_state != s) { m_state = s; emit daemonMiningStateChanged(); } } bool reducedMotion() const { return m_reducedMotion; } void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } } + // Solo mining is a start_mining RPC to whichever daemon the wallet is + // connected to. Against a remote node that is either refused outright or — + // worse, if the operator left the RPC open — it puts someone else's machine + // to work on our address. It is only ever ours to ask of our own node. + bool canMine() const { return m_canMine; } + void setCanMine(bool c) { if (m_canMine != c) { m_canMine = c; emit canMineChanged(); } } signals: void daemonMiningStateChanged(); void reducedMotionChanged(); + void canMineChanged(); void daemonOutput(const QString &line); void syncStatus(int from, int to, int pct); void uptimeChanged(const QString &uptime); @@ -34,6 +44,7 @@ signals: private: int m_state = 0; // 0:idle 1:startup 2:syncing 3:mining bool m_reducedMotion = false; + bool m_canMine = false; }; // wowlet: the pixel-art mining tab. Hosts mining.qml and wires its pick-axe to the wallet's solo @@ -41,7 +52,7 @@ private: class MiningWidget : public QWidget { Q_OBJECT public: - explicit MiningWidget(Wallet *wallet, QWidget *parent = nullptr); + explicit MiningWidget(Wallet *wallet, Nodes *nodes, QWidget *parent = nullptr); private slots: void onStartMining(); @@ -50,6 +61,7 @@ private slots: private: Wallet *m_wallet; + Nodes *m_nodes; QQuickWidget *m_quickWidget; MiningBackend *m_backend; QTimer *m_timer;