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:
@@ -12,6 +12,7 @@
|
||||
#include "../../config/settings.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../util/address_validation.h" // isValidRecipientAddress — payout validation (M-01)
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.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;
|
||||
// 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.
|
||||
// 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 &&
|
||||
std::string(s_pool_worker).empty();
|
||||
(poolPayoutStr.empty() ||
|
||||
(poolPayoutStr != "x" && !util::isValidRecipientAddress(poolPayoutStr)));
|
||||
bool disabled = s_pool_mode
|
||||
? (isToggling || poolBlockedBySolo || poolNeedsPayout)
|
||||
: (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 est_hours_2 = EstimateHoursToBlock(estHashrate, mining.networkHashrate, mining.difficulty);
|
||||
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);
|
||||
else
|
||||
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[] = {
|
||||
{ TR("mining_today"), todayVal, todaySub, greenCol2 },
|
||||
{ TR("mining_yesterday"), yesterdayVal, yesterdaySub, 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++) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "mining_mode_toggle.h"
|
||||
#include "mining_tab_helpers.h"
|
||||
#include "mining_tab.h" // CancelMiningBenchmark (L-11)
|
||||
#include "mining_pool_panel.h"
|
||||
|
||||
#include "../../app.h"
|
||||
@@ -16,6 +17,7 @@
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../../util/address_validation.h" // isValidRecipientAddress — payout validation (M-01)
|
||||
#include "../layout.h"
|
||||
#include "../notifications.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
@@ -101,6 +103,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
s_pool_mode = false;
|
||||
app->settings()->setPoolMode(false);
|
||||
app->settings()->save();
|
||||
CancelMiningBenchmark(app); // don't leave a pool benchmark running after switching to solo (L-11)
|
||||
app->stopPoolMining();
|
||||
}
|
||||
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);
|
||||
if (currentWorkerStr.empty()) {
|
||||
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 {
|
||||
material::Tooltip("%s", TR("mining_payout_tooltip"));
|
||||
}
|
||||
|
||||
@@ -55,6 +55,19 @@ bool IsMiningBenchmarkActive() {
|
||||
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
|
||||
// the background so the "Update" button can show it. Network call to the project Gitea, started
|
||||
// the first time the pool section is shown.
|
||||
|
||||
@@ -21,5 +21,12 @@ void RenderMiningTab(App* app);
|
||||
*/
|
||||
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 dragonx
|
||||
|
||||
@@ -60,7 +60,7 @@ double EstimateHoursToBlock(double localHashrate, double networkHashrate, double
|
||||
{
|
||||
(void)difficulty;
|
||||
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;
|
||||
if (share <= 0.0) return 0.0;
|
||||
return 1.0 / (blocksPerHour * share);
|
||||
|
||||
Reference in New Issue
Block a user