fix(mining): remediate mining-tab audit (22 findings) — crash-safety, async control, validation, math

Fixes all 22 confirmed findings from the mining-tab audit (10 Medium, 12 Low; 0 Critical/High),
adversarially reviewed (6 follow-ups found + fixed, incl. the review-caught idle-auto-start bypass
and a wrong benchmark-restore condition).

Crash-safety & lifecycle:
- M-04: join a stale/finished monitor thread in XmrigManager::start() and ~XmrigManager so an xmrig
  crash-then-restart (or quit) no longer std::terminate()s the wallet.
- L-03/L-10: surface an unexpected miner exit once and clear the stale running flag.

UI never blocks (M-03/L-06/L-08/L-09/L-13): pool start/stop now run on a dedicated serialized FIFO
mining-control thread (joined before teardown), so the ~13 call sites don't block the render thread on
stop()'s SIGTERM->SIGKILL->join; the spawn result marshals back to the UI.

Miner-process / pool trust boundary:
- M-01: validate the payout address (util::isValidRecipientAddress) at EVERY start path — the UI gate
  AND App::startPoolMining() (idle auto-start / thread scaling) — so a stale/wrong-chain address can't
  silently lose rewards.
- M-09: SSRF guard skips the background pool-stats GET for loopback/private/link-local/single-label hosts.
- M-02/L-02: cap the pool-stats + xmrig-API HTTP response bodies.
- L-01: write the xmrig config 0600 at creation (POSIX open with mode) — no world/group-readable window.
- M-10: reject shell-metacharacter binary paths before the version popen (excluding '()' so Program Files
  (x86) still works).

Solo mining: M-06/M-08 clamp thread count to [1, cores] at the setgenerate/xmrig boundary; M-07 notify +
don't lie on stop failure.

Correctness: L-05 block-time constant 75->150s (chainparams); M-05 discloses pool-mode "Est. Daily" as a
rough solo-equivalent; L-04/L-11/L-12 benchmark lifecycle (cancel on nav-away / mode-switch with restore,
skip rebalance mid-benchmark); L-07 honor cancel mid-extract in both the xmrig and daemon updaters.

Two new i18n keys back-filled across all 8 languages; CJK subset font rebuilt for the new glyphs.
Verified across full-node, lite, and Windows builds; tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 14:53:46 -05:00
parent 6ee81a5abe
commit 06afbee4f8
24 changed files with 285 additions and 47 deletions

Binary file not shown.

View File

@@ -886,6 +886,7 @@
"mining_difficulty_copied": "Schwierigkeit kopiert", "mining_difficulty_copied": "Schwierigkeit kopiert",
"mining_est_block": "Gesch. Block", "mining_est_block": "Gesch. Block",
"mining_est_daily": "Gesch. täglich", "mining_est_daily": "Gesch. täglich",
"mining_est_daily_pool_sub": "grobe Solo-Äquivalenz, vor Pool-Gebühr",
"mining_filter_all": "Alle", "mining_filter_all": "Alle",
"mining_filter_tip_all": "Alle Einnahmen anzeigen", "mining_filter_tip_all": "Alle Einnahmen anzeigen",
"mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen", "mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen",
@@ -914,6 +915,7 @@
"mining_open_in_explorer": "Im Explorer öffnen", "mining_open_in_explorer": "Im Explorer öffnen",
"mining_payout_address": "Auszahlungsadresse", "mining_payout_address": "Auszahlungsadresse",
"mining_payout_foreign": "⚠ Diese Auszahlungsadresse befindet sich nicht in Ihrer aktuellen Wallet — geschürfte Belohnungen würden an eine andere Wallet gehen. Aktualisieren Sie sie, wenn Sie die Wallet gewechselt haben.", "mining_payout_foreign": "⚠ Diese Auszahlungsadresse befindet sich nicht in Ihrer aktuellen Wallet — geschürfte Belohnungen würden an eine andere Wallet gehen. Aktualisieren Sie sie, wenn Sie die Wallet gewechselt haben.",
"mining_payout_invalid": "Keine gültige DragonX-Adresse — vor dem Start korrigieren, sonst gehen die Mining-Belohnungen verloren.",
"mining_payout_tooltip": "Adresse für Mining-Belohnungen", "mining_payout_tooltip": "Adresse für Mining-Belohnungen",
"mining_pool": "Pool", "mining_pool": "Pool",
"mining_pool_fee": "Gebühr", "mining_pool_fee": "Gebühr",

View File

@@ -886,6 +886,7 @@
"mining_difficulty_copied": "Dificultad copiada", "mining_difficulty_copied": "Dificultad copiada",
"mining_est_block": "Bloque Est.", "mining_est_block": "Bloque Est.",
"mining_est_daily": "Diario Est.", "mining_est_daily": "Diario Est.",
"mining_est_daily_pool_sub": "equivalente solo aproximado, antes de la comisión del pool",
"mining_filter_all": "Todos", "mining_filter_all": "Todos",
"mining_filter_tip_all": "Mostrar todas las ganancias", "mining_filter_tip_all": "Mostrar todas las ganancias",
"mining_filter_tip_pool": "Mostrar solo ganancias del pool", "mining_filter_tip_pool": "Mostrar solo ganancias del pool",
@@ -914,6 +915,7 @@
"mining_open_in_explorer": "Abrir en explorador", "mining_open_in_explorer": "Abrir en explorador",
"mining_payout_address": "Dirección de Pago", "mining_payout_address": "Dirección de Pago",
"mining_payout_foreign": "⚠ Esta dirección de pago no está en tu cartera actual — las recompensas minadas irían a otra cartera. Actualízala si cambiaste de cartera.", "mining_payout_foreign": "⚠ Esta dirección de pago no está en tu cartera actual — las recompensas minadas irían a otra cartera. Actualízala si cambiaste de cartera.",
"mining_payout_invalid": "No es una dirección DragonX válida — corrígela antes de empezar, o se pierden las recompensas de minería.",
"mining_payout_tooltip": "Dirección para recibir recompensas de minería", "mining_payout_tooltip": "Dirección para recibir recompensas de minería",
"mining_pool": "Pool", "mining_pool": "Pool",
"mining_pool_fee": "Comisión", "mining_pool_fee": "Comisión",

View File

@@ -886,6 +886,7 @@
"mining_difficulty_copied": "Difficulté copiée", "mining_difficulty_copied": "Difficulté copiée",
"mining_est_block": "Bloc est.", "mining_est_block": "Bloc est.",
"mining_est_daily": "Est. quotidien", "mining_est_daily": "Est. quotidien",
"mining_est_daily_pool_sub": "équivalent solo approximatif, avant les frais du pool",
"mining_filter_all": "Tout", "mining_filter_all": "Tout",
"mining_filter_tip_all": "Afficher tous les gains", "mining_filter_tip_all": "Afficher tous les gains",
"mining_filter_tip_pool": "Afficher uniquement les gains du pool", "mining_filter_tip_pool": "Afficher uniquement les gains du pool",
@@ -914,6 +915,7 @@
"mining_open_in_explorer": "Ouvrir dans l'explorateur", "mining_open_in_explorer": "Ouvrir dans l'explorateur",
"mining_payout_address": "Adresse de paiement", "mining_payout_address": "Adresse de paiement",
"mining_payout_foreign": "⚠ Cette adresse de paiement ne fait pas partie de votre portefeuille actuel — les récompenses minées iraient vers un autre portefeuille. Mettez-la à jour si vous avez changé de portefeuille.", "mining_payout_foreign": "⚠ Cette adresse de paiement ne fait pas partie de votre portefeuille actuel — les récompenses minées iraient vers un autre portefeuille. Mettez-la à jour si vous avez changé de portefeuille.",
"mining_payout_invalid": "Adresse DragonX invalide — corrigez-la avant de démarrer, sinon les récompenses de minage sont perdues.",
"mining_payout_tooltip": "Adresse pour recevoir les récompenses de minage", "mining_payout_tooltip": "Adresse pour recevoir les récompenses de minage",
"mining_pool": "Pool", "mining_pool": "Pool",
"mining_pool_fee": "Frais", "mining_pool_fee": "Frais",

View File

@@ -886,6 +886,7 @@
"mining_difficulty_copied": "難易度をコピーしました", "mining_difficulty_copied": "難易度をコピーしました",
"mining_est_block": "予測ブロック", "mining_est_block": "予測ブロック",
"mining_est_daily": "予測日収", "mining_est_daily": "予測日収",
"mining_est_daily_pool_sub": "おおよそのソロ換算(プール手数料前)",
"mining_filter_all": "すべて", "mining_filter_all": "すべて",
"mining_filter_tip_all": "すべての収益を表示", "mining_filter_tip_all": "すべての収益を表示",
"mining_filter_tip_pool": "プール収益のみ表示", "mining_filter_tip_pool": "プール収益のみ表示",
@@ -914,6 +915,7 @@
"mining_open_in_explorer": "エクスプローラーで開く", "mining_open_in_explorer": "エクスプローラーで開く",
"mining_payout_address": "支払いアドレス", "mining_payout_address": "支払いアドレス",
"mining_payout_foreign": "⚠ この支払いアドレスは現在のウォレットに含まれていません — マイニング報酬が別のウォレットに送られます。ウォレットを切り替えた場合は更新してください。", "mining_payout_foreign": "⚠ この支払いアドレスは現在のウォレットに含まれていません — マイニング報酬が別のウォレットに送られます。ウォレットを切り替えた場合は更新してください。",
"mining_payout_invalid": "有効な DragonX アドレスではありません — 開始前に修正してください。さもないとマイニング報酬が失われます。",
"mining_payout_tooltip": "マイニング報酬の受取アドレス", "mining_payout_tooltip": "マイニング報酬の受取アドレス",
"mining_pool": "プール", "mining_pool": "プール",
"mining_pool_fee": "手数料", "mining_pool_fee": "手数料",

View File

@@ -885,6 +885,7 @@
"mining_difficulty_copied": "난이도가 복사되었습니다", "mining_difficulty_copied": "난이도가 복사되었습니다",
"mining_est_block": "예상 블록", "mining_est_block": "예상 블록",
"mining_est_daily": "예상 일일 수익", "mining_est_daily": "예상 일일 수익",
"mining_est_daily_pool_sub": "대략적인 솔로 환산, 풀 수수료 전",
"mining_filter_all": "전체", "mining_filter_all": "전체",
"mining_filter_tip_all": "모든 수익 표시", "mining_filter_tip_all": "모든 수익 표시",
"mining_filter_tip_pool": "풀 수익만 표시", "mining_filter_tip_pool": "풀 수익만 표시",
@@ -913,6 +914,7 @@
"mining_open_in_explorer": "탐색기에서 열기", "mining_open_in_explorer": "탐색기에서 열기",
"mining_payout_address": "지급 주소", "mining_payout_address": "지급 주소",
"mining_payout_foreign": "⚠ 이 지급 주소는 현재 지갑에 없습니다 — 채굴한 보상이 다른 지갑으로 전송됩니다. 지갑을 전환했다면 주소를 업데이트하세요.", "mining_payout_foreign": "⚠ 이 지급 주소는 현재 지갑에 없습니다 — 채굴한 보상이 다른 지갑으로 전송됩니다. 지갑을 전환했다면 주소를 업데이트하세요.",
"mining_payout_invalid": "유효한 DragonX 주소가 아닙니다 — 시작하기 전에 수정하세요. 그렇지 않으면 채굴 보상이 사라집니다.",
"mining_payout_tooltip": "채굴 보상 수신 주소", "mining_payout_tooltip": "채굴 보상 수신 주소",
"mining_pool": "풀", "mining_pool": "풀",
"mining_pool_fee": "수수료", "mining_pool_fee": "수수료",

View File

@@ -886,6 +886,7 @@
"mining_difficulty_copied": "Dificuldade copiada", "mining_difficulty_copied": "Dificuldade copiada",
"mining_est_block": "Bloco Est.", "mining_est_block": "Bloco Est.",
"mining_est_daily": "Est. Diário", "mining_est_daily": "Est. Diário",
"mining_est_daily_pool_sub": "equivalente solo aproximado, antes da taxa do pool",
"mining_filter_all": "Todos", "mining_filter_all": "Todos",
"mining_filter_tip_all": "Mostrar todos os ganhos", "mining_filter_tip_all": "Mostrar todos os ganhos",
"mining_filter_tip_pool": "Mostrar apenas ganhos do pool", "mining_filter_tip_pool": "Mostrar apenas ganhos do pool",
@@ -914,6 +915,7 @@
"mining_open_in_explorer": "Abrir no explorador", "mining_open_in_explorer": "Abrir no explorador",
"mining_payout_address": "Endereço de Pagamento", "mining_payout_address": "Endereço de Pagamento",
"mining_payout_foreign": "⚠ Este endereço de pagamento não está na sua carteira atual — as recompensas mineradas iriam para uma carteira diferente. Atualize-o se você trocou de carteira.", "mining_payout_foreign": "⚠ Este endereço de pagamento não está na sua carteira atual — as recompensas mineradas iriam para uma carteira diferente. Atualize-o se você trocou de carteira.",
"mining_payout_invalid": "Endereço DragonX inválido — corrija antes de iniciar, ou as recompensas de mineração serão perdidas.",
"mining_payout_tooltip": "Endereço para receber recompensas de mineração", "mining_payout_tooltip": "Endereço para receber recompensas de mineração",
"mining_pool": "Pool", "mining_pool": "Pool",
"mining_pool_fee": "Taxa", "mining_pool_fee": "Taxa",

View File

@@ -886,6 +886,7 @@
"mining_difficulty_copied": "Сложность скопирована", "mining_difficulty_copied": "Сложность скопирована",
"mining_est_block": "Расч. блок", "mining_est_block": "Расч. блок",
"mining_est_daily": "Расч. за день", "mining_est_daily": "Расч. за день",
"mining_est_daily_pool_sub": "примерный соло-эквивалент, до комиссии пула",
"mining_filter_all": "Все", "mining_filter_all": "Все",
"mining_filter_tip_all": "Показать все доходы", "mining_filter_tip_all": "Показать все доходы",
"mining_filter_tip_pool": "Показать только доходы пула", "mining_filter_tip_pool": "Показать только доходы пула",
@@ -914,6 +915,7 @@
"mining_open_in_explorer": "Открыть в обозревателе", "mining_open_in_explorer": "Открыть в обозревателе",
"mining_payout_address": "Адрес выплат", "mining_payout_address": "Адрес выплат",
"mining_payout_foreign": "⚠ Этот адрес выплат отсутствует в вашем текущем кошельке — намайненные вознаграждения будут отправлены в другой кошелёк. Обновите его, если вы сменили кошелёк.", "mining_payout_foreign": "⚠ Этот адрес выплат отсутствует в вашем текущем кошельке — намайненные вознаграждения будут отправлены в другой кошелёк. Обновите его, если вы сменили кошелёк.",
"mining_payout_invalid": "Недействительный адрес DragonX — исправьте перед запуском, иначе награды за майнинг будут потеряны.",
"mining_payout_tooltip": "Адрес для получения вознаграждений за майнинг", "mining_payout_tooltip": "Адрес для получения вознаграждений за майнинг",
"mining_pool": "Пул", "mining_pool": "Пул",
"mining_pool_fee": "Комиссия", "mining_pool_fee": "Комиссия",

View File

@@ -885,6 +885,7 @@
"mining_difficulty_copied": "难度已复制", "mining_difficulty_copied": "难度已复制",
"mining_est_block": "预计区块", "mining_est_block": "预计区块",
"mining_est_daily": "预计日收益", "mining_est_daily": "预计日收益",
"mining_est_daily_pool_sub": "粗略的单人挖矿等值,扣除矿池费用前",
"mining_filter_all": "全部", "mining_filter_all": "全部",
"mining_filter_tip_all": "显示所有收益", "mining_filter_tip_all": "显示所有收益",
"mining_filter_tip_pool": "仅显示矿池收益", "mining_filter_tip_pool": "仅显示矿池收益",
@@ -913,6 +914,7 @@
"mining_open_in_explorer": "在浏览器中打开", "mining_open_in_explorer": "在浏览器中打开",
"mining_payout_address": "支付地址", "mining_payout_address": "支付地址",
"mining_payout_foreign": "⚠ 此支付地址不在您当前的钱包中——挖矿奖励将进入另一个钱包。如果您切换过钱包,请更新它。", "mining_payout_foreign": "⚠ 此支付地址不在您当前的钱包中——挖矿奖励将进入另一个钱包。如果您切换过钱包,请更新它。",
"mining_payout_invalid": "不是有效的 DragonX 地址——启动前请更正,否则挖矿奖励将丢失。",
"mining_payout_tooltip": "接收挖矿奖励的地址", "mining_payout_tooltip": "接收挖矿奖励的地址",
"mining_pool": "矿池", "mining_pool": "矿池",
"mining_pool_fee": "费用", "mining_pool_fee": "费用",

View File

@@ -147,6 +147,45 @@ void App::wipeSecrets()
sodium_memzero(import_key_input_, sizeof(import_key_input_)); // pasted private key (SECRET) sodium_memzero(import_key_input_, sizeof(import_key_input_)); // pasted private key (SECRET)
} }
// Enqueue a blocking xmrig start/stop op onto the dedicated serialized control thread so the render thread
// never blocks on stop()'s SIGTERM->SIGKILL->join, while start/stop still execute in FIFO order. (M-03/…)
void App::postMiningControl(std::function<void()> job)
{
{
std::lock_guard<std::mutex> lk(mining_ctl_mutex_);
if (mining_ctl_stop_) return; // shutting down — don't enqueue new mining ops
if (!mining_ctl_thread_.joinable()) {
mining_ctl_thread_ = std::thread([this]() {
for (;;) {
std::function<void()> j;
{
std::unique_lock<std::mutex> lk(mining_ctl_mutex_);
mining_ctl_cv_.wait(lk, [this]{ return mining_ctl_stop_ || !mining_ctl_queue_.empty(); });
if (mining_ctl_stop_) return; // abandon any pending jobs on shutdown
j = std::move(mining_ctl_queue_.front());
mining_ctl_queue_.pop_front();
}
j();
}
});
}
mining_ctl_queue_.push_back(std::move(job));
}
mining_ctl_cv_.notify_one();
}
// Signal the mining-control thread to stop and join it. Called at shutdown BEFORE xmrig_manager_ is stopped
// or destroyed, so no control job runs concurrently with teardown. Idempotent.
void App::stopMiningControlThread()
{
{
std::lock_guard<std::mutex> lk(mining_ctl_mutex_);
mining_ctl_stop_ = true;
}
mining_ctl_cv_.notify_all();
if (mining_ctl_thread_.joinable()) mining_ctl_thread_.join();
}
namespace { namespace {
// How often auto-balance re-evaluates the pool while active. Long, because switching // How often auto-balance re-evaluates the pool while active. Long, because switching
// restarts the miner (drops in-flight shares + reconnect); the incumbent stickiness // restarts the miner (drops in-flight shares + reconnect); the incumbent stickiness
@@ -166,6 +205,7 @@ void App::updatePoolAutoBalance()
if (!supportsPoolMining()) return; // pool mining is available in both builds (solo is full-node only) if (!supportsPoolMining()) return; // pool mining is available in both builds (solo is full-node only)
if (settings_->getPoolSelectMode() != config::Settings::PoolSelectMode::AutoBalance) return; if (settings_->getPoolSelectMode() != config::Settings::PoolSelectMode::AutoBalance) return;
if (!settings_->getPoolMode()) return; // only while POOL mode is selected if (!settings_->getPoolMode()) return; // only while POOL mode is selected
if (ui::IsMiningBenchmarkActive()) return; // don't auto-switch pools mid-benchmark — it restarts xmrig at the wrong thread count (L-12)
const long long now = steadyNowMs(); const long long now = steadyNowMs();
const bool intervalDue = (last_balance_eval_ms_ == 0) || const bool intervalDue = (last_balance_eval_ms_ == 0) ||
@@ -965,6 +1005,20 @@ void App::update()
} }
} }
// Surface an unexpected miner exit (crash / OOM-kill / external SIGKILL) once, and clear the stale
// running flag so the UI and auto-balance don't keep believing it's still hashing. (L-03, L-10)
if (xmrig_manager_ && state_.pool_mining.xmrig_running
&& xmrig_manager_->getState() == daemon::XmrigManager::State::Error) {
state_.pool_mining.xmrig_running = false;
state_.pool_mining.hashrate_10s = 0.0;
state_.pool_mining.hashrate_60s = 0.0;
state_.pool_mining.hashrate_15m = 0.0;
pool_starting_.store(false, std::memory_order_relaxed);
const std::string err = xmrig_manager_->getLastError();
ui::Notifications::instance().error(err.empty() ? "Miner stopped unexpectedly."
: ("Miner stopped: " + err));
}
// Poll xmrig stats every ~2 seconds (use a simple toggle) // Poll xmrig stats every ~2 seconds (use a simple toggle)
static bool xmrig_poll_tick = false; static bool xmrig_poll_tick = false;
xmrig_poll_tick = !xmrig_poll_tick; xmrig_poll_tick = !xmrig_poll_tick;
@@ -1797,6 +1851,10 @@ void App::render()
if ((current_page_ == ui::NavPage::Console || current_page_ == ui::NavPage::LiteConsole) if ((current_page_ == ui::NavPage::Console || current_page_ == ui::NavPage::LiteConsole)
&& settings_ && settings_->getConsoleAutoFocus()) && settings_ && settings_->getConsoleAutoFocus())
console_tab_.requestInputFocus(); console_tab_.requestInputFocus();
// Leaving the Mining tab → cancel a running thread benchmark so the miner isn't abandoned at a
// benchmark step. (L-04)
if (prev_page_ == ui::NavPage::Mining && current_page_ != ui::NavPage::Mining)
ui::CancelMiningBenchmark(this);
prev_page_ = current_page_; prev_page_ = current_page_;
} }
if (page_alpha_ < 1.0f) { if (page_alpha_ < 1.0f) {
@@ -5595,6 +5653,10 @@ void App::beginShutdown()
fast_worker_->requestStop(); fast_worker_->requestStop();
} }
// Drain + join the mining-control thread FIRST so no async start/stop job runs while we tear the miner
// down here (avoids two threads driving xmrig_manager_ during shutdown). (M-03 cluster)
stopMiningControlThread();
// Stop xmrig pool miner before stopping the daemon // Stop xmrig pool miner before stopping the daemon
if (xmrig_manager_ && xmrig_manager_->isRunning()) { if (xmrig_manager_ && xmrig_manager_->isRunning()) {
shutdown_status_ = "Stopping pool miner..."; shutdown_status_ = "Stopping pool miner...";
@@ -6380,6 +6442,10 @@ void App::renderLoadingOverlay(float contentH)
void App::shutdown() void App::shutdown()
{ {
// Ensure the mining-control thread is stopped + joined (idempotent; beginShutdown already did it on the
// normal quit path, but shutdown() can also run without it). Must precede xmrig_manager_ teardown.
stopMiningControlThread();
// Wipe any copied secret from the OS clipboard before we exit — the 45s auto-clear timer // Wipe any copied secret from the OS clipboard before we exit — the 45s auto-clear timer
// never fires if the user quits sooner, which would otherwise leave a key/seed resident. // never fires if the user quits sooner, which would otherwise leave a key/seed resident.
// (ImGui context is still alive here; App::shutdown() runs before ImGui::DestroyContext().) // (ImGui context is still alive here; App::shutdown() runs before ImGui::DestroyContext().)

View File

@@ -14,6 +14,7 @@
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <deque> #include <deque>
#include <condition_variable>
#include <nlohmann/json_fwd.hpp> #include <nlohmann/json_fwd.hpp>
#include "data/transaction_history_cache.h" #include "data/transaction_history_cache.h"
#include "data/address_book.h" #include "data/address_book.h"
@@ -855,6 +856,16 @@ private:
bool lite_startup_lock_checked_ = false; bool lite_startup_lock_checked_ = false;
std::unique_ptr<daemon::DaemonController> daemon_controller_; std::unique_ptr<daemon::DaemonController> daemon_controller_;
std::unique_ptr<daemon::XmrigManager> xmrig_manager_; std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
// Serialized async mining-control queue: xmrig start/stop (SIGTERM->SIGKILL->join, up to ~3s) run on
// this dedicated FIFO thread instead of the render thread, so the UI never blocks and stop/start
// ordering is preserved across the ~13 call sites. (M-03/L-06/L-08/L-09/L-13)
std::thread mining_ctl_thread_;
std::mutex mining_ctl_mutex_;
std::condition_variable mining_ctl_cv_;
std::deque<std::function<void()>> mining_ctl_queue_;
bool mining_ctl_stop_ = false;
void postMiningControl(std::function<void()> job); // enqueue a blocking xmrig op onto the FIFO thread
void stopMiningControlThread(); // signal + join the control thread (shutdown)
// Auto-balance runtime state (pool mining, full-node only). The service fetches // Auto-balance runtime state (pool mining, full-node only). The service fetches
// pool hashrates off-thread; the RNG drives the weighted-random pick. // pool hashrates off-thread; the RNG drives the weighted-random pick.
util::PoolStatsService pool_stats_service_; util::PoolStatsService pool_stats_service_;

View File

@@ -63,6 +63,7 @@
#include <cstdio> // popen the rebuild helper #include <cstdio> // popen the rebuild helper
#include "util/perf_log.h" #include "util/perf_log.h"
#include "util/i18n.h" #include "util/i18n.h"
#include "util/address_validation.h" // isValidRecipientAddress — payout validation at every start path (M-01)
#include "util/secure_vault.h" #include "util/secure_vault.h"
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@@ -2338,6 +2339,10 @@ void App::startMining(int threads)
return; return;
} }
if (!state_.connected || !rpc_ || !worker_) return; if (!state_.connected || !rpc_ || !worker_) return;
// Clamp the requested thread count to [1, logical cores] before setgenerate — an unclamped value
// (from a settings field or idle-scaling) would ask the daemon to spawn arbitrarily many threads. (M-08)
const int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
threads = std::clamp(threads, 1, maxThreads);
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
worker_->post([this, threads]() -> rpc::RPCWorker::MainCb { worker_->post([this, threads]() -> rpc::RPCWorker::MainCb {
@@ -2372,19 +2377,25 @@ void App::stopMining()
worker_->post([this]() -> rpc::RPCWorker::MainCb { worker_->post([this]() -> rpc::RPCWorker::MainCb {
bool ok = false; bool ok = false;
std::string errMsg;
try { try {
rpc::RPCClient::TraceScope trace("Mining tab / Stop mining"); rpc::RPCClient::TraceScope trace("Mining tab / Stop mining");
rpc_->call("setgenerate", {false, 0}); rpc_->call("setgenerate", {false, 0});
ok = true; ok = true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
DEBUG_LOGF("Failed to stop mining: %s\n", e.what()); errMsg = e.what();
DEBUG_LOGF("Failed to stop mining: %s\n", errMsg.c_str());
} }
return [this, ok]() { return [this, ok, errMsg]() {
mining_toggle_in_progress_.store(false); mining_toggle_in_progress_.store(false);
if (ok) { if (ok) {
state_.mining.generate = false; state_.mining.generate = false;
state_.mining.localHashrate = 0.0; state_.mining.localHashrate = 0.0;
DEBUG_LOGF("Mining stopped\n"); DEBUG_LOGF("Mining stopped\n");
} else {
// Don't silently leave generate=true as if it worked: tell the user and let the next
// getmininginfo refresh reconcile the true daemon state. (M-07)
ui::Notifications::instance().error("Failed to stop mining: " + errMsg);
} }
}; };
}); });
@@ -2397,21 +2408,28 @@ void App::startPoolMining(int threads)
ui::Notifications::instance().warning("Pool mining is unavailable in this build"); ui::Notifications::instance().warning("Pool mining is unavailable in this build");
return; return;
} }
// Clamp to [1, logical cores] before the count reaches xmrig (M-06/M-08 pool path).
threads = std::clamp(threads, 1, std::max(1, (int)std::thread::hardware_concurrency()));
if (!xmrig_manager_) if (!xmrig_manager_)
xmrig_manager_ = std::make_unique<daemon::XmrigManager>(); xmrig_manager_ = std::make_unique<daemon::XmrigManager>();
// If already running, stop first (e.g. thread count change) // Stop solo mining first if active (async via the RPC worker).
if (xmrig_manager_->isRunning()) {
xmrig_manager_->stop();
}
// Stop solo mining first if active
if (state_.mining.generate) stopMining(); if (state_.mining.generate) stopMining();
// (the "stop the already-running miner first" step is done inside the control job below, in FIFO order)
daemon::XmrigManager::Config cfg; daemon::XmrigManager::Config cfg;
cfg.pool_url = settings_->getPoolUrl(); cfg.pool_url = settings_->getPoolUrl();
cfg.worker_name = settings_->getPoolWorker(); cfg.worker_name = settings_->getPoolWorker();
// Validate the payout address at EVERY start entry point (manual Start button, idle auto-start, thread
// scaling) — not just the UI gate — since a stale/hand-edited/wrong-chain address here silently loses
// mining rewards. (M-01) worker_name IS the pool login the rewards are credited to (see below).
if (!cfg.worker_name.empty() && cfg.worker_name != "x" &&
!util::isValidRecipientAddress(cfg.worker_name)) {
ui::Notifications::instance().error(
"Pool payout address is not a valid DragonX address — mining not started.");
return;
}
// The algo follows the pool: official pools use their own algo (pool.dragonx.cc // The algo follows the pool: official pools use their own algo (pool.dragonx.cc
// needs rx/dragonx, pool.dragonx.is rx/hush); custom hosts keep the setting. // needs rx/dragonx, pool.dragonx.is rx/hush); custom hosts keep the setting.
cfg.algo = util::resolvePoolAlgo(cfg.pool_url, settings_->getPoolAlgo()); cfg.algo = util::resolvePoolAlgo(cfg.pool_url, settings_->getPoolAlgo());
@@ -2443,34 +2461,46 @@ void App::startPoolMining(int threads)
return; return;
} }
if (!xmrig_manager_->start(cfg)) { // Run the blocking stop(if running)+start on the serialized mining-control thread so the render thread
std::string err = xmrig_manager_->getLastError(); // never blocks on stop()'s SIGTERM->SIGKILL->join; marshal the spawn result back to the UI. (M-03/L-06/
DEBUG_LOGF("[ERROR] Pool mining: %s\n", err.c_str()); // L-08/L-09/L-13). cfg was fully built above on this (main) thread.
daemon::XmrigManager::Config cfgCopy = cfg;
// Check for Windows Defender blocking (error 225 = ERROR_VIRUS_INFECTED) postMiningControl([this, cfgCopy]() {
if (err.find("error 225") != std::string::npos || if (xmrig_manager_->isRunning()) xmrig_manager_->stop(3000);
err.find("virus") != std::string::npos) { const bool ok = xmrig_manager_->start(cfgCopy);
ui::Notifications::instance().error( const std::string err = ok ? std::string() : xmrig_manager_->getLastError();
"Windows Defender blocked xmrig. Add exclusion for %APPDATA%\\ObsidianDragon"); if (!worker_) return;
worker_->post([this, ok, err]() -> rpc::RPCWorker::MainCb {
return [this, ok, err]() {
if (ok) {
// Miner spawned — it still needs a few seconds to connect to the pool and start hashing.
pool_starting_.store(true, std::memory_order_relaxed);
ui::Notifications::instance().info("Starting pool miner — connecting to the pool…");
} else {
DEBUG_LOGF("[ERROR] Pool mining: %s\n", err.c_str());
// Windows Defender blocking (error 225 = ERROR_VIRUS_INFECTED)
if (err.find("error 225") != std::string::npos || err.find("virus") != std::string::npos) {
ui::Notifications::instance().error(
"Windows Defender blocked xmrig. Add exclusion for %APPDATA%\\ObsidianDragon");
#ifdef _WIN32 #ifdef _WIN32
// Offer to open Windows Security settings pending_antivirus_dialog_ = true;
pending_antivirus_dialog_ = true;
#endif #endif
} else { } else {
ui::Notifications::instance().error("Failed to start pool miner: " + err); ui::Notifications::instance().error("Failed to start pool miner: " + err);
} }
} else { }
// Miner spawned — it still needs a few seconds to connect to the pool and start hashing. };
pool_starting_.store(true, std::memory_order_relaxed); });
ui::Notifications::instance().info("Starting pool miner — connecting to the pool…"); });
}
} }
void App::stopPoolMining() void App::stopPoolMining()
{ {
if (xmrig_manager_ && xmrig_manager_->isRunning()) { if (!xmrig_manager_) return;
xmrig_manager_->stop(3000); // Off the render thread — stop()'s SIGTERM->SIGKILL->join can block up to ~3s. (M-03/L-06/L-08/L-09)
} postMiningControl([this]() {
if (xmrig_manager_->isRunning()) xmrig_manager_->stop(3000);
});
} }
// ============================================================================ // ============================================================================

View File

@@ -759,6 +759,10 @@ void App::checkIdleMining() {
// Resolve auto values: active defaults to half, idle defaults to all // Resolve auto values: active defaults to half, idle defaults to all
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2); if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
if (idleThreads <= 0) idleThreads = maxThreads; if (idleThreads <= 0) idleThreads = maxThreads;
// Clamp to [1, logical cores] before these reach setgenerate / startPoolMining — a settings field
// could otherwise carry an arbitrary count straight past every bound. (M-06)
activeThreads = std::clamp(activeThreads, 1, maxThreads);
idleThreads = std::clamp(idleThreads, 1, maxThreads);
if (systemIdle) { if (systemIdle) {
// System is idle — scale up to idle thread count // System is idle — scale up to idle thread count

View File

@@ -89,8 +89,32 @@ static std::string getConfigDir() {
// libcurl write callback // libcurl write callback
static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) { static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) {
auto* s = static_cast<std::string*>(userdata); auto* s = static_cast<std::string*>(userdata);
s->append(static_cast<char*>(ptr), sz * n); const size_t add = sz * n;
return sz * n; // Stats JSON (local xmrig HTTP API + pool API) is tiny; refuse an unbounded body from a hostile or
// MITM'd endpoint so it can't grow this string until OOM. Returning < add aborts the transfer. (L-02)
constexpr size_t kMaxStatsBytes = 1u << 20; // 1 MiB
if (s->size() + add > kMaxStatsBytes) return 0;
s->append(static_cast<char*>(ptr), add);
return add;
}
// True if `host` (already stripped of scheme+port) is a loopback/private/link-local/single-label target
// that a public mining pool would never be — used to refuse a background stats GET to it (M-09).
static bool hostLooksInternal(const std::string& host) {
if (host.empty() || host == "localhost") return true;
if (host.rfind("127.", 0) == 0 || host.rfind("10.", 0) == 0 ||
host.rfind("192.168.", 0) == 0 || host.rfind("169.254.", 0) == 0) return true;
if (host.rfind("172.", 0) == 0) { // 172.16.0.0 - 172.31.255.255
const int second = std::atoi(host.c_str() + 4);
if (second >= 16 && second <= 31) return true;
}
if (host.find(':') != std::string::npos) { // IPv6 literal: loopback / ULA / link-local
if (host == "::1" || host.rfind("fc", 0) == 0 || host.rfind("fd", 0) == 0 ||
host.rfind("fe80", 0) == 0) return true;
}
if (host.size() >= 6 && host.compare(host.size() - 6, 6, ".local") == 0) return true;
if (host.find('.') == std::string::npos) return true; // bare single-label name = LAN/hosts, not a pool
return false;
} }
// ============================================================================ // ============================================================================
@@ -100,9 +124,14 @@ static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) {
XmrigManager::XmrigManager() = default; XmrigManager::XmrigManager() = default;
XmrigManager::~XmrigManager() { XmrigManager::~XmrigManager() {
should_stop_ = true;
if (isRunning()) { if (isRunning()) {
stop(3000); stop(3000);
} }
// Join a monitor thread left joinable by an unexpected xmrig exit (State::Error, so isRunning() is
// false and stop() above was skipped) — std::thread's destructor would otherwise std::terminate(). (M-04)
if (monitor_thread_.joinable())
monitor_thread_.join();
} }
// ============================================================================ // ============================================================================
@@ -208,21 +237,41 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
try { try {
fs::create_directories(fs::path(outPath).parent_path()); fs::create_directories(fs::path(outPath).parent_path());
const std::string dumped = j.dump(4);
#ifndef _WIN32
// Create the config 0600 AT CREATION (open with mode) so the API token + wallet address are never
// in a world/group-readable file — even for a local attacker who opened it in the old
// create-then-chmod window and held the fd open across the chmod. (L-01)
int fd = ::open(outPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd < 0) {
setLastError("Cannot write xmrig config: " + outPath);
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
return false;
}
size_t off = 0;
bool wrote = true;
while (off < dumped.size()) {
ssize_t nw = ::write(fd, dumped.data() + off, dumped.size() - off);
if (nw <= 0) { wrote = false; break; }
off += static_cast<size_t>(nw);
}
::close(fd);
if (!wrote) {
setLastError("Cannot write xmrig config: " + outPath);
return false;
}
return true;
#else
std::ofstream ofs(outPath, std::ios::trunc); std::ofstream ofs(outPath, std::ios::trunc);
if (!ofs.is_open()) { if (!ofs.is_open()) {
setLastError("Cannot write xmrig config: " + outPath); setLastError("Cannot write xmrig config: " + outPath);
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str()); DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
return false; return false;
} }
#ifndef _WIN32 ofs << dumped;
// Restrict to owner (0600) BEFORE writing any secret material (API token, wallet
// address, worker name). The file is still empty here, so the config is never
// world-readable — closing the window between creation and the previous post-write chmod.
chmod(outPath.c_str(), 0600);
#endif
ofs << j.dump(4);
ofs.close(); ofs.close();
return true; return true;
#endif
} catch (const std::exception& e) { } catch (const std::exception& e) {
setLastError(std::string("Config write error: ") + e.what()); setLastError(std::string("Config write error: ") + e.what());
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str()); DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
@@ -296,7 +345,11 @@ bool XmrigManager::start(const Config& cfg) {
return false; return false;
} }
// Start monitor thread // Join a prior monitor thread before move-assigning: if xmrig exited unexpectedly, monitorProcess set
// State::Error and returned, leaving monitor_thread_ joinable — move-assigning over a joinable
// std::thread calls std::terminate() and aborts the whole wallet. (M-04)
if (monitor_thread_.joinable())
monitor_thread_.join();
monitor_thread_ = std::thread(&XmrigManager::monitorProcess, this); monitor_thread_ = std::thread(&XmrigManager::monitorProcess, this);
state_ = State::Running; state_ = State::Running;
DEBUG_LOGF("[INFO] XmrigManager: started\n"); DEBUG_LOGF("[INFO] XmrigManager: started\n");
@@ -783,6 +836,11 @@ void XmrigManager::fetchPoolApiStats() {
// own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore // own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore
// /api/pools); unknown/custom hosts fall back to the .is convention. // /api/pools); unknown/custom hosts fall back to the .is convention.
const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_); const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_);
// SSRF guard: for an UNKNOWN (user-typed) pool host, don't let the wallet issue a background GET to a
// loopback/private/link-local/single-label target — those aren't public mining pools, and a
// paste-a-pool-config lure could otherwise point us at an internal host. Known pools use their trusted
// registry statsUrl and are exempt. (M-09)
if (!known && hostLooksInternal(pool_host_)) return;
const std::string url = known ? known->statsUrl const std::string url = known ? known->statsUrl
: ("https://" + pool_host_ + "/api/stats"); : ("https://" + pool_host_ + "/api/stats");
@@ -861,7 +919,14 @@ void XmrigManager::startVersionDetection()
std::thread([]() { std::thread([]() {
const std::string bin = findXmrigBinary(); const std::string bin = findXmrigBinary();
std::string ver; std::string ver;
if (!bin.empty()) { // Don't hand a path containing shell/cmd metacharacters to popen()'s shell — bin is normally an
// app-controlled path, but this closes command injection if it ever isn't. (M-10)
// Reject only chars that stay shell-special INSIDE the double-quotes we wrap bin in ("\"" + bin + "\"")
// on cmd.exe or /bin/sh. Parens are inert when quoted, so they're excluded — otherwise common Windows
// paths like "C:\Program Files (x86)\..." would be rejected and version detection would silently fail. (M-10)
const bool binShellSafe =
!bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos;
if (binShellSafe) {
const std::string cmd = "\"" + bin + "\" --version 2>&1"; const std::string cmd = "\"" + bin + "\" --version 2>&1";
#ifdef _WIN32 #ifdef _WIN32
FILE* fp = _popen(cmd.c_str(), "r"); FILE* fp = _popen(cmd.c_str(), "r");

View File

@@ -12,6 +12,7 @@
#include "../../config/settings.h" #include "../../config/settings.h"
#include "../../util/i18n.h" #include "../../util/i18n.h"
#include "../../util/platform.h" #include "../../util/platform.h"
#include "../../util/address_validation.h" // isValidRecipientAddress — payout validation (M-01)
#include "../schema/ui_schema.h" #include "../schema/ui_schema.h"
#include "../material/type.h" #include "../material/type.h"
#include "../material/draw_helpers.h" #include "../material/draw_helpers.h"
@@ -685,8 +686,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running; bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running;
// Can't start pool mining without a payout address (blank for a new wallet with no z-address); // Can't start pool mining without a payout address (blank for a new wallet with no z-address);
// only blocks starting — stopping a running miner stays enabled. // only blocks starting — stopping a running miner stays enabled.
// Block start when the payout address is empty OR not a valid DragonX address — mining to a
// malformed / wrong-chain address silently loses the rewards. (M-01)
const std::string poolPayoutStr(s_pool_worker);
bool poolNeedsPayout = s_pool_mode && !state.pool_mining.xmrig_running && bool poolNeedsPayout = s_pool_mode && !state.pool_mining.xmrig_running &&
std::string(s_pool_worker).empty(); (poolPayoutStr.empty() ||
(poolPayoutStr != "x" && !util::isValidRecipientAddress(poolPayoutStr)));
bool disabled = s_pool_mode bool disabled = s_pool_mode
? (isToggling || poolBlockedBySolo || poolNeedsPayout) ? (isToggling || poolBlockedBySolo || poolNeedsPayout)
: (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing)); : (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing));

View File

@@ -104,7 +104,10 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
} }
} }
// Use pool hashrate for EST. DAILY when in pool mode // Est. Daily = expected reward for YOUR own hashrate share of the network. In pool mode this uses
// your local miner rate (pool_mining.hashrate_10s), NOT the pool's aggregate rate: pool payouts are
// share-proportional, so your expected daily is the same solo-equivalent value (a rough estimate,
// before the pool fee). Using the pool's total rate here would show the POOL's earnings, not yours. (M-05)
double estHashrate = s_pool_mode ? state.pool_mining.hashrate_10s : mining.localHashrate; double estHashrate = s_pool_mode ? state.pool_mining.hashrate_10s : mining.localHashrate;
double est_hours_2 = EstimateHoursToBlock(estHashrate, mining.networkHashrate, mining.difficulty); double est_hours_2 = EstimateHoursToBlock(estHashrate, mining.networkHashrate, mining.difficulty);
double estDailyBlocks = (est_hours_2 > 0) ? (24.0 / est_hours_2) : 0.0; double estDailyBlocks = (est_hours_2 > 0) ? (24.0 / est_hours_2) : 0.0;
@@ -218,12 +221,15 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily); snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
else else
snprintf(estVal, sizeof(estVal), "N/A"); snprintf(estVal, sizeof(estVal), "N/A");
// Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the
// number isn't silently mismatched to its plain "Est. Daily" label. (M-05)
const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
EarningsEntry entries[] = { EarningsEntry entries[] = {
{ TR("mining_today"), todayVal, todaySub, greenCol2 }, { TR("mining_today"), todayVal, todaySub, greenCol2 },
{ TR("mining_yesterday"), yesterdayVal, yesterdaySub, OnSurface() }, { TR("mining_yesterday"), yesterdayVal, yesterdaySub, OnSurface() },
{ TR("mining_all_time"), allVal, allSub, OnSurface() }, { TR("mining_all_time"), allVal, allSub, OnSurface() },
{ TR("mining_est_daily"), estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() }, { TR("mining_est_daily"), estVal, estSub, estActive ? greenCol2 : OnSurfaceDisabled() },
}; };
for (int ei = 0; ei < numCols; ei++) { for (int ei = 0; ei < numCols; ei++) {

View File

@@ -4,6 +4,7 @@
#include "mining_mode_toggle.h" #include "mining_mode_toggle.h"
#include "mining_tab_helpers.h" #include "mining_tab_helpers.h"
#include "mining_tab.h" // CancelMiningBenchmark (L-11)
#include "mining_pool_panel.h" #include "mining_pool_panel.h"
#include "../../app.h" #include "../../app.h"
@@ -16,6 +17,7 @@
#include "../material/type.h" #include "../material/type.h"
#include "../material/draw_helpers.h" #include "../material/draw_helpers.h"
#include "../material/colors.h" #include "../material/colors.h"
#include "../../util/address_validation.h" // isValidRecipientAddress — payout validation (M-01)
#include "../layout.h" #include "../layout.h"
#include "../notifications.h" #include "../notifications.h"
#include "../../embedded/IconsMaterialDesign.h" #include "../../embedded/IconsMaterialDesign.h"
@@ -101,6 +103,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
s_pool_mode = false; s_pool_mode = false;
app->settings()->setPoolMode(false); app->settings()->setPoolMode(false);
app->settings()->save(); app->settings()->save();
CancelMiningBenchmark(app); // don't leave a pool benchmark running after switching to solo (L-11)
app->stopPoolMining(); app->stopPoolMining();
} }
if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
@@ -382,6 +385,8 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
std::string currentWorkerStr(s_pool_worker); std::string currentWorkerStr(s_pool_worker);
if (currentWorkerStr.empty()) { if (currentWorkerStr.empty()) {
material::Tooltip("%s", TR("mining_generate_z_address_hint")); material::Tooltip("%s", TR("mining_generate_z_address_hint"));
} else if (currentWorkerStr != "x" && !util::isValidRecipientAddress(currentWorkerStr)) {
material::Tooltip("%s", TR("mining_payout_invalid")); // block start below too (M-01)
} else { } else {
material::Tooltip("%s", TR("mining_payout_tooltip")); material::Tooltip("%s", TR("mining_payout_tooltip"));
} }

View File

@@ -55,6 +55,19 @@ bool IsMiningBenchmarkActive() {
return s_benchmark.active(); return s_benchmark.active();
} }
void CancelMiningBenchmark(App* app) {
if (!s_benchmark.active()) return;
const int restoreThreads = s_benchmark.prev_threads;
s_benchmark.reset();
// Restore the miner to its pre-benchmark thread count. A benchmark runs in pool mode, so restore
// regardless of the instantaneous running state — the sweep may be mid inter-candidate stop, where an
// isPoolMinerRunning() check would be transiently false and silently drop the restart. (L-04, L-11)
if (app && restoreThreads > 0) {
app->stopPoolMining();
app->startPoolMining(restoreThreads);
}
}
// Miner-update version check (one shot per session): fetches the latest DRG-XMRig release tag in // Miner-update version check (one shot per session): fetches the latest DRG-XMRig release tag in
// the background so the "Update" button can show it. Network call to the project Gitea, started // the background so the "Update" button can show it. Network call to the project Gitea, started
// the first time the pool section is shown. // the first time the pool section is shown.

View File

@@ -21,5 +21,12 @@ void RenderMiningTab(App* app);
*/ */
bool IsMiningBenchmarkActive(); bool IsMiningBenchmarkActive();
/**
* @brief Cancel a running thread benchmark and restore the miner to its pre-benchmark thread count.
* Safe to call when no benchmark is active (no-op). Used when leaving the Mining tab or switching to
* solo mode so the miner isn't left stuck at a benchmark step. (L-04, L-11)
*/
void CancelMiningBenchmark(App* app);
} // namespace ui } // namespace ui
} // namespace dragonx } // namespace dragonx

View File

@@ -60,7 +60,7 @@ double EstimateHoursToBlock(double localHashrate, double networkHashrate, double
{ {
(void)difficulty; (void)difficulty;
if (localHashrate <= 0.0 || networkHashrate <= 0.0) return 0.0; if (localHashrate <= 0.0 || networkHashrate <= 0.0) return 0.0;
double blocksPerHour = 3600.0 / 75.0; double blocksPerHour = 3600.0 / 150.0; // DragonX mainnet target spacing is 150s (chainparams) (L-05)
double share = localHashrate / networkHashrate; double share = localHashrate / networkHashrate;
if (share <= 0.0) return 0.0; if (share <= 0.0) return 0.0;
return 1.0 / (blocksPerHour * share); return 1.0 / (blocksPerHour * share);

View File

@@ -354,6 +354,7 @@ void DaemonUpdater::installResolved(const std::string& targetDir, const DaemonRe
bool failed = false; bool failed = false;
const int numFiles = static_cast<int>(mz_zip_reader_get_num_files(&zip)); const int numFiles = static_cast<int>(mz_zip_reader_get_num_files(&zip));
for (int i = 0; i < numFiles && !failed; ++i) { for (int i = 0; i < numFiles && !failed; ++i) {
if (cancel_requested_) { failed = true; break; } // honor cancel mid-extraction so the join returns promptly (L-07)
mz_zip_archive_file_stat st; mz_zip_archive_file_stat st;
if (!mz_zip_reader_file_stat(&zip, i, &st)) continue; if (!mz_zip_reader_file_stat(&zip, i, &st)) continue;
if (mz_zip_reader_is_file_a_directory(&zip, i)) continue; if (mz_zip_reader_is_file_a_directory(&zip, i)) continue;

View File

@@ -1880,6 +1880,8 @@ void I18n::loadBuiltinEnglish()
strings_["mining_open_in_explorer"] = "Open in explorer"; strings_["mining_open_in_explorer"] = "Open in explorer";
strings_["mining_payout_address"] = "Payout Address"; strings_["mining_payout_address"] = "Payout Address";
strings_["mining_payout_tooltip"] = "Address to receive mining rewards"; strings_["mining_payout_tooltip"] = "Address to receive mining rewards";
strings_["mining_payout_invalid"] = "Not a valid DragonX address — fix it before starting, or mining rewards are lost.";
strings_["mining_est_daily_pool_sub"] = "rough solo-equivalent, before pool fee";
strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address"; strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address";
strings_["mining_pool"] = "Pool"; strings_["mining_pool"] = "Pool";
strings_["mining_payout_foreign"] = "⚠ This payout address isn't in your current wallet — mined rewards would go to a different wallet. Update it if you switched wallets."; strings_["mining_payout_foreign"] = "⚠ This payout address isn't in your current wallet — mined rewards would go to a different wallet. Update it if you switched wallets.";

View File

@@ -13,8 +13,14 @@ namespace {
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp) size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
{ {
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb); auto* s = static_cast<std::string*>(userp);
return size * nmemb; const size_t add = size * nmemb;
// Pool stats JSON is tiny; refuse an unbounded body from a hostile/MITM'd endpoint (returning < add
// aborts the transfer) so it can't grow this string until OOM. (M-02)
constexpr size_t kMaxPoolStatsBytes = 1u << 20; // 1 MiB
if (s->size() + add > kMaxPoolStatsBytes) return 0;
s->append(static_cast<char*>(contents), add);
return add;
} }
// Returning non-zero asks libcurl to abort the transfer — used so shutdown doesn't // Returning non-zero asks libcurl to abort the transfer — used so shutdown doesn't

View File

@@ -345,6 +345,7 @@ void XmrigUpdater::installResolved(const std::string& targetDir, const XmrigRele
bool failed = false; bool failed = false;
const int numFiles = static_cast<int>(mz_zip_reader_get_num_files(&zip)); const int numFiles = static_cast<int>(mz_zip_reader_get_num_files(&zip));
for (int i = 0; i < numFiles && !failed; ++i) { for (int i = 0; i < numFiles && !failed; ++i) {
if (cancel_requested_) { failed = true; break; } // honor cancel mid-extraction so the dialog's join returns promptly (L-07)
mz_zip_archive_file_stat st; mz_zip_archive_file_stat st;
if (!mz_zip_reader_file_stat(&zip, i, &st)) continue; if (!mz_zip_reader_file_stat(&zip, i, &st)) continue;
if (mz_zip_reader_is_file_a_directory(&zip, i)) continue; if (mz_zip_reader_is_file_a_directory(&zip, i)) continue;