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:
Binary file not shown.
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "手数料",
|
||||||
|
|||||||
@@ -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": "수수료",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "Комиссия",
|
||||||
|
|||||||
@@ -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": "费用",
|
||||||
|
|||||||
66
src/app.cpp
66
src/app.cpp
@@ -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().)
|
||||||
|
|||||||
11
src/app.h
11
src/app.h
@@ -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_;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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++) {
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user