feat(mining): pool registry, schema-aware stats fetcher, pool-select setting
Data + service layer for supporting multiple mining pools: - pool_registry: KnownPool table (pool.dragonx.is + pool.dragonx.cc) carrying each pool's stratum host, xmrig algo, and stats-API schema; pure helpers findKnownPoolByUrl / resolvePoolAlgo / parsePoolHashrate / chooseWeightedPool (weighted-random by hashrate with incumbent stickiness). - pool_stats_service: background, schema-aware hashrate fetcher (libcurl, TLS, browser UA for Cloudflare) modelled on XmrigUpdater. - settings: pool_select_mode (manual | auto_balance), string-serialized. - i18n strings + unit tests (both stats schemas, algo resolution, weighted pick). Note: pool.dragonx.cc's stratum host is us.dragonx.cc:3333 — the .cc domain is only the Cloudflare-proxied web/API host and does not accept stratum on :3333. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
#include "util/payment_uri.h"
|
||||
#include "util/platform.h"
|
||||
#include "util/xmrig_updater.h"
|
||||
#include "util/pool_registry.h"
|
||||
#include "util/daemon_updater.h"
|
||||
#include "util/lite_server_probe.h"
|
||||
#include "wallet/lite_connection_service.h"
|
||||
@@ -4826,6 +4827,97 @@ void testXmrigLiveInstall()
|
||||
std::filesystem::remove_all(dir, ec);
|
||||
}
|
||||
|
||||
// Mining pool registry: URL matching + per-pool algo resolution.
|
||||
void testPoolRegistryLookup()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
EXPECT_EQ(knownPools().size(), static_cast<std::size_t>(2));
|
||||
|
||||
// The algo follows the pool. Note pool.dragonx.cc's stratum host is us.dragonx.cc
|
||||
// (the .cc domain is only the Cloudflare-proxied web/API host).
|
||||
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/dragonx"));
|
||||
EXPECT_EQ(resolvePoolAlgo("pool.dragonx.is:3433", "rx/dragonx"), std::string("rx/hush"));
|
||||
// Bare host (no port) still matches; scheme + path are tolerated.
|
||||
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc", "rx/hush"), std::string("rx/dragonx"));
|
||||
EXPECT_EQ(resolvePoolAlgo("stratum+tcp://us.dragonx.cc:3333/x", "rx/hush"),
|
||||
std::string("rx/dragonx"));
|
||||
// Unknown host -> fallback algo.
|
||||
EXPECT_EQ(resolvePoolAlgo("my.pool.example:1234", "rx/hush"), std::string("rx/hush"));
|
||||
|
||||
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") != nullptr);
|
||||
EXPECT_TRUE(findKnownPoolByUrl("unknown.host:1") == nullptr);
|
||||
// A mismatched explicit port must NOT match a known pool.
|
||||
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:9999") == nullptr);
|
||||
}
|
||||
|
||||
// Schema-aware pool hashrate parsing (the two pools speak different APIs).
|
||||
void testPoolHashrateParsing()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
bool ok = false;
|
||||
|
||||
// pool.dragonx.is custom schema: pools.dragonx.hashrate
|
||||
const std::string isJson =
|
||||
R"({"pools":{"dragonx":{"hashrate":27670.14,"workerCount":12}},"global":{"hashrate":30000.0}})";
|
||||
double hr = parsePoolHashrate(PoolStatsSchema::DragonXIs, isJson, "", ok);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_NEAR(hr, 27670.14, 0.01);
|
||||
|
||||
// Miningcore schema: the requested pool id selects the entry (not just the first).
|
||||
const std::string ccJson =
|
||||
R"({"pools":[)"
|
||||
R"({"id":"dragonx-solo","poolStats":{"poolHashrate":88780.0}},)"
|
||||
R"({"id":"dragonx-pplns","poolStats":{"poolHashrate":1585.9}}]})";
|
||||
hr = parsePoolHashrate(PoolStatsSchema::Miningcore, ccJson, "dragonx-pplns", ok);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_NEAR(hr, 1585.9, 0.01);
|
||||
hr = parsePoolHashrate(PoolStatsSchema::Miningcore, ccJson, "dragonx-solo", ok);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_NEAR(hr, 88780.0, 0.01);
|
||||
|
||||
// Missing pool id / malformed / wrong-schema input all fail closed (no silent 0).
|
||||
parsePoolHashrate(PoolStatsSchema::Miningcore, ccJson, "does-not-exist", ok);
|
||||
EXPECT_FALSE(ok);
|
||||
parsePoolHashrate(PoolStatsSchema::DragonXIs, "not json", "", ok);
|
||||
EXPECT_FALSE(ok);
|
||||
parsePoolHashrate(PoolStatsSchema::Miningcore, "{}", "dragonx-pplns", ok);
|
||||
EXPECT_FALSE(ok);
|
||||
parsePoolHashrate(PoolStatsSchema::DragonXIs, ccJson, "", ok); // .cc JSON, .is parser
|
||||
EXPECT_FALSE(ok);
|
||||
}
|
||||
|
||||
// Weighted-random pool selection: smaller pools favored, incumbent sticky, fails safe.
|
||||
void testPoolWeightedSelection()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
std::mt19937 rng(12345u); // fixed seed -> deterministic
|
||||
|
||||
std::vector<PoolHashrate> pools = {
|
||||
{"big", 100000.0, true}, // large -> low weight
|
||||
{"small", 1000.0, true}, // small -> high weight (favored)
|
||||
};
|
||||
int smallCount = 0, bigCount = 0;
|
||||
for (int i = 0; i < 2000; ++i) {
|
||||
const std::string pick = chooseWeightedPool(pools, /*currentId=*/"", rng);
|
||||
if (pick == "small") ++smallCount;
|
||||
else if (pick == "big") ++bigCount;
|
||||
}
|
||||
EXPECT_TRUE(smallCount > bigCount * 5); // ~100:1 by weight
|
||||
|
||||
// Only one usable pool -> always chosen; no usable pool -> keep current.
|
||||
std::vector<PoolHashrate> one = {{"a", 5.0, true}, {"b", 1.0, false}};
|
||||
EXPECT_EQ(chooseWeightedPool(one, "", rng), std::string("a"));
|
||||
std::vector<PoolHashrate> none = {{"a", 5.0, false}, {"b", 1.0, false}};
|
||||
EXPECT_EQ(chooseWeightedPool(none, "keepme", rng), std::string("keepme"));
|
||||
|
||||
// Incumbent stickiness: with equal hashrates the current pool wins the majority.
|
||||
std::vector<PoolHashrate> equal = {{"x", 1000.0, true}, {"y", 1000.0, true}};
|
||||
int stay = 0;
|
||||
for (int i = 0; i < 2000; ++i)
|
||||
if (chooseWeightedPool(equal, "x", rng) == "x") ++stay;
|
||||
EXPECT_TRUE(stay > 1000); // >50% thanks to kIncumbentStayBias
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -4903,6 +4995,9 @@ int main()
|
||||
testDaemonReleaseListParsing();
|
||||
testLiteServerHostParsing();
|
||||
testLiteOfficialServerDetection();
|
||||
testPoolRegistryLookup();
|
||||
testPoolHashrateParsing();
|
||||
testPoolWeightedSelection();
|
||||
testAtomicFileWrite();
|
||||
testAddressChecksumValidation();
|
||||
testLiteServerProbeLive();
|
||||
|
||||
Reference in New Issue
Block a user