Skip to content

Commit 24ea76b

Browse files
authored
Merge pull request #1775 from cyrossignol/detect-pools
gui: Add context for when BOINC is attached to a pool
2 parents a794d75 + 1fdc0df commit 24ea76b

File tree

3 files changed

+225
-5
lines changed

3 files changed

+225
-5
lines changed

src/neuralnet/researcher.cpp

+82-4
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,61 @@ const Project* ResolveWhitelistProject(
180180
return nullptr;
181181
}
182182

183+
//!
184+
//! \brief Represents a Gridcoin pool that stakes on behalf of its users.
185+
//!
186+
//! The wallet uses these entries to detect when BOINC is attached to a pool
187+
//! account so that it can provide more useful information in the UI.
188+
//!
189+
class MiningPool
190+
{
191+
public:
192+
MiningPool(const Cpid cpid, std::string m_name, std::string m_url)
193+
: m_cpid(cpid), m_name(std::move(m_name)), m_url(std::move(m_url))
194+
{
195+
}
196+
197+
MiningPool(const std::string& cpid, std::string m_name, std::string m_url)
198+
: MiningPool(Cpid::Parse(cpid), std::move(m_name), std::move(m_url))
199+
{
200+
}
201+
202+
Cpid m_cpid; //!< The pool's external CPID.
203+
std::string m_name; //!< The name of the pool.
204+
std::string m_url; //!< The pool's website URL.
205+
};
206+
207+
//!
208+
//! \brief The set of known Gridcoin pools.
209+
//!
210+
//! TODO: In the future, we may add a contract type that allows pool operators
211+
//! to register a pool via the blockchain. The static list gets us by for now.
212+
//!
213+
const MiningPool g_pools[] = {
214+
{ "7d0d73fe026d66fd4ab8d5d8da32a611", "grcpool.com", "https://grcpool.com/" },
215+
{ "a914eba952be5dfcf73d926b508fd5fa", "grcpool.com-2", "https://grcpool.com/" },
216+
{ "163f049997e8a2dee054d69a7720bf05", "grcpool.com-3", "https://grcpool.com/" },
217+
{ "326bb50c0dd0ba9d46e15fae3484af35", "Arikado", "https://gridcoinpool.ru/" },
218+
};
219+
220+
//!
221+
//! \brief Determine whether the provided CPID belongs to a Gridcoin pool.
222+
//!
223+
//! \param cpid An external CPID for a project loaded from BOINC.
224+
//!
225+
//! \return \c true if the CPID matches a known Gridcoin pool's CPID.
226+
//!
227+
bool IsPoolCpid(const Cpid cpid)
228+
{
229+
for (const auto& pool : g_pools) {
230+
if (pool.m_cpid == cpid) {
231+
return true;
232+
}
233+
}
234+
235+
return false;
236+
}
237+
183238
//!
184239
//! \brief Fetch the contents of BOINC's client_state.xml file from disk.
185240
//!
@@ -315,6 +370,9 @@ void TryProjectCpid(MiningId& mining_id, const MiningProject& project)
315370
case MiningProject::Error::INVALID_TEAM:
316371
LogPrintf("Project %s's team is not whitelisted.", project.m_name);
317372
return;
373+
case MiningProject::Error::POOL:
374+
LogPrintf("Project %s is attached to a pool.", project.m_name);
375+
return;
318376
}
319377

320378
mining_id = project.m_cpid;
@@ -411,13 +469,16 @@ void StoreResearcher(Researcher context)
411469
case ResearcherStatus::ACTIVE:
412470
msMiningErrors = _("Eligible for Research Rewards");
413471
break;
472+
case ResearcherStatus::POOL:
473+
msMiningErrors = _("Staking Only - Pool Detected");
474+
break;
414475
case ResearcherStatus::NO_PROJECTS:
415476
msMiningErrors = _("Staking Only - No Eligible Research Projects");
416477
break;
417478
case ResearcherStatus::NO_BEACON:
418479
msMiningErrors = _("Staking Only - No active beacon");
419480
break;
420-
default:
481+
case ResearcherStatus::INVESTOR:
421482
msMiningErrors = _("Staking Only - Investor Mode");
422483
break;
423484
}
@@ -770,11 +831,16 @@ MiningProject MiningProject::Parse(const std::string& xml)
770831
ExtractXML(xml, "<team_name>", "</team_name>"),
771832
ExtractXML(xml, "<master_url>", "</master_url>"));
772833

834+
if (IsPoolCpid(project.m_cpid) && !GetBoolArg("-pooloperator", false)) {
835+
project.m_error = MiningProject::Error::POOL;
836+
return project;
837+
}
838+
773839
if (project.m_cpid.IsZero()) {
774840
const std::string external_cpid
775841
= ExtractXML(xml, "<external_cpid>", "</external_cpid>");
776842

777-
// A bug in BOINC sometimes results in an empty external CPID element
843+
// Old BOINC server versions may not provide an external CPID element
778844
// in client_state.xml. For these cases, we'll recompute the external
779845
// CPID of the project from the internal CPID and email address:
780846
//
@@ -833,15 +899,17 @@ std::string MiningProject::ErrorMessage() const
833899
case Error::INVALID_TEAM: return _("Invalid team");
834900
case Error::MALFORMED_CPID: return _("Malformed CPID");
835901
case Error::MISMATCHED_CPID: return _("Project email mismatch");
836-
default: return _("Unknown error");
902+
case Error::POOL: return _("Pool");
837903
}
904+
905+
return _("Unknown error");
838906
}
839907

840908
// -----------------------------------------------------------------------------
841909
// Class: MiningProjectMap
842910
// -----------------------------------------------------------------------------
843911

844-
MiningProjectMap::MiningProjectMap()
912+
MiningProjectMap::MiningProjectMap() : m_has_pool_project(false)
845913
{
846914
}
847915

@@ -883,6 +951,11 @@ bool MiningProjectMap::empty() const
883951
return m_projects.empty();
884952
}
885953

954+
bool MiningProjectMap::ContainsPool() const
955+
{
956+
return m_has_pool_project;
957+
}
958+
886959
ProjectOption MiningProjectMap::Try(const std::string& name) const
887960
{
888961
const auto iter = m_projects.find(name);
@@ -896,6 +969,7 @@ ProjectOption MiningProjectMap::Try(const std::string& name) const
896969

897970
void MiningProjectMap::Set(MiningProject project)
898971
{
972+
m_has_pool_project |= project.m_error == MiningProject::Error::POOL;
899973
m_projects.emplace(project.m_name, std::move(project));
900974
}
901975

@@ -1155,6 +1229,10 @@ ResearcherStatus Researcher::Status() const
11551229
}
11561230

11571231
if (!m_projects.empty()) {
1232+
if (m_projects.ContainsPool()) {
1233+
return ResearcherStatus::POOL;
1234+
}
1235+
11581236
return ResearcherStatus::NO_PROJECTS;
11591237
}
11601238

src/neuralnet/researcher.h

+14
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ enum class ResearcherStatus
2828
{
2929
INVESTOR, //!< BOINC not present; ineligible for research rewards.
3030
ACTIVE, //!< CPID eligible for research rewards.
31+
POOL, //!< BOINC attached to projects for a Gridcoin mining pool.
3132
NO_PROJECTS, //!< BOINC present, but no eligible projects (investor).
3233
NO_BEACON, //!< No active beacon public key advertised.
3334
};
@@ -50,6 +51,7 @@ struct MiningProject
5051
INVALID_TEAM, //!< Project not joined to a whitelisted team.
5152
MALFORMED_CPID, //!< Failed to parse a valid external CPID.
5253
MISMATCHED_CPID, //!< External CPID failed internal CPID + email test.
54+
POOL, //!< External CPID matches a Gridcoin pool.
5355
};
5456

5557
//!
@@ -172,6 +174,13 @@ class MiningProjectMap
172174
//!
173175
bool empty() const;
174176

177+
//!
178+
//! \brief Determine whether the map contains a project attached to a pool.
179+
//!
180+
//! \return \c true if a project in the map has a pool CPID.
181+
//!
182+
bool ContainsPool() const;
183+
175184
//!
176185
//! \brief Try to get the loaded BOINC project with the specified name.
177186
//!
@@ -206,6 +215,11 @@ class MiningProjectMap
206215
//! \brief Stores the local BOINC projects loaded from client_state.xml.
207216
//!
208217
ProjectStorage m_projects;
218+
219+
//!
220+
//! \brief Caches whether the map contains a project attached to a pool.
221+
//!
222+
bool m_has_pool_project;
209223
}; // MiningProjectMap
210224

211225
class Researcher; // forward for ResearcherPtr

src/test/neuralnet/researcher_tests.cpp

+129-1
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,25 @@ BOOST_AUTO_TEST_CASE(it_determines_whether_a_project_is_eligible)
258258
BOOST_CHECK(project.Eligible() == false);
259259
}
260260

261+
BOOST_AUTO_TEST_CASE(it_detects_projects_with_pool_cpids)
262+
{
263+
// The XML string contains a subset of data found within a <project> element
264+
// from BOINC's client_state.xml file:
265+
//
266+
NN::MiningProject project = NN::MiningProject::Parse(
267+
R"XML(
268+
<project>
269+
<master_url>https://example.com/</master_url>
270+
<project_name>Project Name</project_name>
271+
<team_name>Team Name</team_name>
272+
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
273+
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
274+
</project>
275+
)XML");
276+
277+
BOOST_CHECK(project.m_error == NN::MiningProject::Error::POOL);
278+
}
279+
261280
BOOST_AUTO_TEST_CASE(it_determines_whether_a_project_is_whitelisted)
262281
{
263282
NN::MiningProject project("project name", NN::Cpid(), "team name", "url");
@@ -464,6 +483,17 @@ BOOST_AUTO_TEST_CASE(it_indicates_whether_it_contains_any_projects)
464483
BOOST_CHECK(projects.empty() == false);
465484
}
466485

486+
BOOST_AUTO_TEST_CASE(it_indicates_whether_it_contains_any_pool_projects)
487+
{
488+
NN::MiningProjectMap projects;
489+
NN::MiningProject project("project name", NN::Cpid(), "team name", "url");
490+
491+
project.m_error = NN::MiningProject::Error::POOL;
492+
projects.Set(std::move(project));
493+
494+
BOOST_CHECK(projects.ContainsPool() == true);
495+
}
496+
467497
BOOST_AUTO_TEST_CASE(it_fetches_a_project_by_name)
468498
{
469499
NN::MiningProjectMap projects;
@@ -855,6 +885,15 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml)
855885
<external_cpid>f5d8234352e5a5ae3915debba7258294</external_cpid>
856886
</project>
857887
)XML",
888+
// Pool CPID:
889+
R"XML(
890+
<project>
891+
<master_url>https://example.com/</master_url>
892+
<project_name>Project Name 7</project_name>
893+
<team_name>Gridcoin</team_name>
894+
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
895+
</project>
896+
)XML",
858897
}));
859898

860899
NN::Cpid cpid = NN::Cpid::Parse("f5d8234352e5a5ae3915debba7258294");
@@ -863,7 +902,7 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml)
863902
BOOST_CHECK(NN::Researcher::Get()->Id() == NN::MiningId::ForInvestor());
864903

865904
const NN::MiningProjectMap& projects = NN::Researcher::Get()->Projects();
866-
BOOST_CHECK(projects.size() == 6);
905+
BOOST_CHECK(projects.size() == 7);
867906

868907
if (const NN::ProjectOption project1 = projects.Try("project name 1")) {
869908
BOOST_CHECK(project1->m_name == "project name 1");
@@ -925,6 +964,14 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml)
925964
BOOST_FAIL("Project 6 does not exist in the mining project map.");
926965
}
927966

967+
if (const NN::ProjectOption project6 = projects.Try("project name 7")) {
968+
BOOST_CHECK(project6->m_name == "project name 7");
969+
BOOST_CHECK(project6->m_error == NN::MiningProject::Error::POOL);
970+
BOOST_CHECK(project6->Eligible() == false);
971+
} else {
972+
BOOST_FAIL("Project 7 does not exist in the mining project map.");
973+
}
974+
928975
// Clean up:
929976
SetArgument("email", "");
930977
NN::Researcher::Reload(NN::MiningProjectMap());
@@ -1484,4 +1531,85 @@ BOOST_AUTO_TEST_CASE(it_resets_to_investor_mode_when_explicitly_configured,
14841531
NN::Researcher::Reload(NN::MiningProjectMap());
14851532
}
14861533

1534+
BOOST_AUTO_TEST_CASE(it_resets_to_investor_when_it_only_finds_pool_projects)
1535+
{
1536+
const NN::Cpid cpid = NN::Cpid::Parse("f5d8234352e5a5ae3915debba7258294");
1537+
SetArgument("email", "[email protected]");
1538+
AddTestBeacon(cpid);
1539+
1540+
// External CPID is a pool CPID:
1541+
NN::Researcher::Reload(NN::MiningProjectMap::Parse({
1542+
R"XML(
1543+
<project>
1544+
<master_url>https://example.com/</master_url>
1545+
<project_name>Pool Project</project_name>
1546+
<team_name>Gridcoin</team_name>
1547+
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
1548+
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
1549+
</project>
1550+
)XML",
1551+
}));
1552+
1553+
BOOST_CHECK(NN::Researcher::Get()->Id() == NN::MiningId::ForInvestor());
1554+
BOOST_CHECK(NN::Researcher::Get()->Eligible() == false);
1555+
BOOST_CHECK(NN::Researcher::Get()->Status() == NN::ResearcherStatus::POOL);
1556+
1557+
NN::Researcher::Reload(NN::MiningProjectMap::Parse({
1558+
R"XML(
1559+
<project>
1560+
<master_url>https://example.com/</master_url>
1561+
<project_name>My Project</project_name>
1562+
<team_name>Gridcoin</team_name>
1563+
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
1564+
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
1565+
</project>
1566+
)XML",
1567+
R"XML(
1568+
<project>
1569+
<master_url>https://example.com/</master_url>
1570+
<project_name>Pool Project</project_name>
1571+
<team_name>Gridcoin</team_name>
1572+
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
1573+
<external_cpid>f5d8234352e5a5ae3915debba7258294</external_cpid>
1574+
</project>
1575+
)XML",
1576+
}));
1577+
1578+
BOOST_CHECK(NN::Researcher::Get()->Id() == cpid);
1579+
BOOST_CHECK(NN::Researcher::Get()->Eligible() == true);
1580+
BOOST_CHECK(NN::Researcher::Get()->Status() != NN::ResearcherStatus::POOL);
1581+
1582+
// Clean up:
1583+
SetArgument("email", "");
1584+
RemoveTestBeacon(cpid);
1585+
NN::Researcher::Reload(NN::MiningProjectMap());
1586+
}
1587+
1588+
BOOST_AUTO_TEST_CASE(it_allows_pool_operators_to_load_pool_cpids)
1589+
{
1590+
SetArgument("pooloperator", "1");
1591+
1592+
// External CPID is a pool CPID:
1593+
NN::Researcher::Reload(NN::MiningProjectMap::Parse({
1594+
R"XML(
1595+
<project>
1596+
<master_url>https://example.com/</master_url>
1597+
<project_name>Name</project_name>
1598+
<team_name>Gridcoin</team_name>
1599+
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
1600+
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
1601+
</project>
1602+
)XML",
1603+
}));
1604+
1605+
// We can't completely test that a pool CPID loads, but we can check that
1606+
// the it didn't fail because of the pool CPID:
1607+
//
1608+
BOOST_CHECK(NN::Researcher::Get()->Status() != NN::ResearcherStatus::POOL);
1609+
1610+
// Clean up:
1611+
SetArgument("pooloperator", "0");
1612+
NN::Researcher::Reload(NN::MiningProjectMap());
1613+
}
1614+
14871615
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)