fix(mining): only offer to mine on a node that is ours

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
This commit is contained in:
jwinterm
2026-09-04 05:36:45 -04:00
parent 081c01ada9
commit c7fca31653
4 changed files with 77 additions and 6 deletions
+1 -1
View File
@@ -252,7 +252,7 @@ void MainWindow::initWidgets() {
}); });
// [Mining] — wowlet: the pixel-art mining tab (mining.qml + embedded-node solo miner) // [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"); ui->tabWidget->addTab(m_miningWidget, "Mining");
// [Notes] // [Notes]
+53 -3
View File
@@ -512,6 +512,10 @@ Rectangle {
Image { Image {
id: imgAxe id: imgAxe
visible: mining.daemonMiningState === 0 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" source: "qrc:/mining/axe.png"
width: 73 width: 73
height: 75 height: 75
@@ -551,7 +555,11 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if(mining.daemonMiningState === 0) { if (mining.daemonMiningState === 0) {
if (!mining.canMine) {
remoteNodeNotice.flash();
return;
}
root.startMining(); root.startMining();
root.calcAvailableHeightConsoleLines(); root.calcAvailableHeightConsoleLines();
} }
@@ -559,14 +567,56 @@ Rectangle {
root.stopMining(); root.stopMining();
} }
onEntered: { onEntered: {
imgAxe.height = 64 if (mining.canMine) {
imgAxe.width = 64 imgAxe.height = 64
imgAxe.width = 64
} else {
remoteNodeNotice.show();
}
} }
onExited: { onExited: {
imgAxe.height = 75 imgAxe.height = 75
imgAxe.width = 73 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() }
}
} }
} }
} }
+10 -1
View File
@@ -3,6 +3,8 @@
#include "MiningWidget.h" #include "MiningWidget.h"
#include "utils/nodes.h"
#include <algorithm> #include <algorithm>
#include <QVBoxLayout> #include <QVBoxLayout>
@@ -20,9 +22,10 @@ static QString formatUptime(qint64 secs) {
.arg(secs % 60, 2, 10, QChar('0')); .arg(secs % 60, 2, 10, QChar('0'));
} }
MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent) MiningWidget::MiningWidget(Wallet *wallet, Nodes *nodes, QWidget *parent)
: QWidget(parent) : QWidget(parent)
, m_wallet(wallet) , m_wallet(wallet)
, m_nodes(nodes)
, m_quickWidget(new QQuickWidget(this)) , m_quickWidget(new QQuickWidget(this))
, m_backend(new MiningBackend(this)) , m_backend(new MiningBackend(this))
, m_timer(new QTimer(this)) , m_timer(new QTimer(this))
@@ -46,6 +49,10 @@ MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent)
} }
void MiningWidget::onStartMining() { 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); int threads = std::max(1, QThread::idealThreadCount() / 2);
if (m_wallet->startMining(threads)) if (m_wallet->startMining(threads))
m_backend->setState(3); m_backend->setState(3);
@@ -57,6 +64,8 @@ void MiningWidget::onStopMining() {
} }
void MiningWidget::poll() { void MiningWidget::poll() {
m_backend->setCanMine(m_nodes && m_nodes->connection().isLocal());
bool mining = m_wallet->isMining(); bool mining = m_wallet->isMining();
m_backend->setState(mining ? 3 : 0); m_backend->setState(mining ? 3 : 0);
+13 -1
View File
@@ -5,6 +5,8 @@
#define WOWLET_MININGWIDGET_H #define WOWLET_MININGWIDGET_H
#include <QWidget> #include <QWidget>
class Nodes;
#include <QTimer> #include <QTimer>
#include <QQuickWidget> #include <QQuickWidget>
@@ -16,16 +18,24 @@ class MiningBackend : public QObject {
Q_OBJECT Q_OBJECT
Q_PROPERTY(int daemonMiningState READ daemonMiningState NOTIFY daemonMiningStateChanged) Q_PROPERTY(int daemonMiningState READ daemonMiningState NOTIFY daemonMiningStateChanged)
Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged) Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged)
Q_PROPERTY(bool canMine READ canMine NOTIFY canMineChanged)
public: public:
explicit MiningBackend(QObject *parent = nullptr) : QObject(parent) {} explicit MiningBackend(QObject *parent = nullptr) : QObject(parent) {}
int daemonMiningState() const { return m_state; } int daemonMiningState() const { return m_state; }
void setState(int s) { if (m_state != s) { m_state = s; emit daemonMiningStateChanged(); } } void setState(int s) { if (m_state != s) { m_state = s; emit daemonMiningStateChanged(); } }
bool reducedMotion() const { return m_reducedMotion; } bool reducedMotion() const { return m_reducedMotion; }
void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } } 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: signals:
void daemonMiningStateChanged(); void daemonMiningStateChanged();
void reducedMotionChanged(); void reducedMotionChanged();
void canMineChanged();
void daemonOutput(const QString &line); void daemonOutput(const QString &line);
void syncStatus(int from, int to, int pct); void syncStatus(int from, int to, int pct);
void uptimeChanged(const QString &uptime); void uptimeChanged(const QString &uptime);
@@ -34,6 +44,7 @@ signals:
private: private:
int m_state = 0; // 0:idle 1:startup 2:syncing 3:mining int m_state = 0; // 0:idle 1:startup 2:syncing 3:mining
bool m_reducedMotion = false; 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 // 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 { class MiningWidget : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit MiningWidget(Wallet *wallet, QWidget *parent = nullptr); explicit MiningWidget(Wallet *wallet, Nodes *nodes, QWidget *parent = nullptr);
private slots: private slots:
void onStartMining(); void onStartMining();
@@ -50,6 +61,7 @@ private slots:
private: private:
Wallet *m_wallet; Wallet *m_wallet;
Nodes *m_nodes;
QQuickWidget *m_quickWidget; QQuickWidget *m_quickWidget;
MiningBackend *m_backend; MiningBackend *m_backend;
QTimer *m_timer; QTimer *m_timer;