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>
126 lines
4.2 KiB
C++
126 lines
4.2 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "pool_stats_service.h"
|
|
|
|
#include <curl/curl.h>
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
|
|
namespace {
|
|
|
|
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
|
|
{
|
|
auto* s = static_cast<std::string*>(userp);
|
|
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
|
|
// block on an in-flight fetch.
|
|
int xferInfoCb(void* clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t)
|
|
{
|
|
const auto* self = static_cast<const PoolStatsService*>(clientp);
|
|
return (self && self->cancelRequested()) ? 1 : 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
PoolStatsService::~PoolStatsService()
|
|
{
|
|
cancel_requested_ = true;
|
|
if (worker_.joinable()) worker_.join();
|
|
}
|
|
|
|
void PoolStatsService::refresh(const std::vector<KnownPool>& pools)
|
|
{
|
|
if (worker_running_.exchange(true)) return; // already refreshing
|
|
if (worker_.joinable()) worker_.join(); // reap the previous finished worker
|
|
worker_ = std::thread([this, pools]() {
|
|
run(pools);
|
|
worker_running_ = false;
|
|
});
|
|
}
|
|
|
|
std::string PoolStatsService::httpGet(const std::string& url)
|
|
{
|
|
CURL* curl = curl_easy_init();
|
|
if (!curl) return {};
|
|
|
|
std::string result;
|
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
|
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
// pool.dragonx.cc sits behind Cloudflare and 403s odd User-Agents — present a
|
|
// browser-like UA and accept compressed responses.
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT,
|
|
"Mozilla/5.0 (compatible; ObsidianDragon)");
|
|
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 8L);
|
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L);
|
|
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
|
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
|
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferInfoCb);
|
|
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this);
|
|
|
|
struct curl_slist* hdrs = nullptr;
|
|
hdrs = curl_slist_append(hdrs, "Accept: application/json");
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
|
|
|
|
const CURLcode res = curl_easy_perform(curl);
|
|
long httpCode = 0;
|
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
|
|
curl_slist_free_all(hdrs);
|
|
curl_easy_cleanup(curl);
|
|
|
|
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) return {};
|
|
return result;
|
|
}
|
|
|
|
void PoolStatsService::run(std::vector<KnownPool> pools)
|
|
{
|
|
std::map<std::string, PoolHashrate> results;
|
|
for (const auto& p : pools) {
|
|
if (cancel_requested_.load()) break;
|
|
PoolHashrate hr;
|
|
hr.id = p.id;
|
|
const std::string body = httpGet(p.statsUrl);
|
|
if (!body.empty()) {
|
|
bool ok = false;
|
|
const double v = parsePoolHashrate(p.schema, body, p.miningcorePoolId, ok);
|
|
hr.ok = ok;
|
|
hr.hashrateHs = ok ? v : 0.0;
|
|
|
|
bool feeOk = false;
|
|
const double fee = parsePoolFee(p.schema, body, p.miningcorePoolId, feeOk);
|
|
// Only trust a sane fee; anything else leaves feePercent < 0 so the UI
|
|
// falls back to the compile-time KnownPool.feePercent.
|
|
if (feeOk && fee >= 0.0 && fee <= 100.0) hr.feePercent = fee;
|
|
}
|
|
results[p.id] = hr;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lk(mutex_);
|
|
snapshot_.byId = std::move(results);
|
|
snapshot_.ready = true;
|
|
}
|
|
|
|
PoolStatsService::Snapshot PoolStatsService::snapshot() const
|
|
{
|
|
std::lock_guard<std::mutex> lk(mutex_);
|
|
return snapshot_;
|
|
}
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|