Skip to content

Commit c2ccd9a

Browse files
committed
Add context for when BOINC is attached to a pool
This adds detection for Gridcoin pool accounts when loading the researcher context to select a CPID. It allows the UI to supply more useful information for pool users. For now, this embeds pool CPIDs in the wallet. We may expand on this in the future to allow pool operators to submit a contract to register a pool through the blockchain.
1 parent b52a5fa commit c2ccd9a

File tree

3 files changed

+225
-5
lines changed

3 files changed

+225
-5
lines changed

src/neuralnet/researcher.cpp

Lines changed: 82 additions & 4 deletions
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("-pool", 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

Lines changed: 14 additions & 0 deletions
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

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,25 @@ BOOST_AUTO_TEST_CASE(it_determines_whether_a_project_is_eligible)
249249
BOOST_CHECK(project.Eligible() == false);
250250
}
251251

252+
BOOST_AUTO_TEST_CASE(it_detects_projects_with_pool_cpids)
253+
{
254+
// The XML string contains a subset of data found within a <project> element
255+
// from BOINC's client_state.xml file:
256+
//
257+
NN::MiningProject project = NN::MiningProject::Parse(
258+
R"XML(
259+
<project>
260+
<master_url>https://example.com/</master_url>
261+
<project_name>Project Name</project_name>
262+
<team_name>Team Name</team_name>
263+
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
264+
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
265+
</project>
266+
)XML");
267+
268+
BOOST_CHECK(project.m_error == NN::MiningProject::Error::POOL);
269+
}
270+
252271
BOOST_AUTO_TEST_CASE(it_determines_whether_a_project_is_whitelisted)
253272
{
254273
NN::MiningProject project("project name", NN::Cpid(), "team name", "url");
@@ -455,6 +474,17 @@ BOOST_AUTO_TEST_CASE(it_indicates_whether_it_contains_any_projects)
455474
BOOST_CHECK(projects.empty() == false);
456475
}
457476

477+
BOOST_AUTO_TEST_CASE(it_indicates_whether_it_contains_any_pool_projects)
478+
{
479+
NN::MiningProjectMap projects;
480+
NN::MiningProject project("project name", NN::Cpid(), "team name", "url");
481+
482+
project.m_error = NN::MiningProject::Error::POOL;
483+
projects.Set(std::move(project));
484+
485+
BOOST_CHECK(projects.ContainsPool() == true);
486+
}
487+
458488
BOOST_AUTO_TEST_CASE(it_fetches_a_project_by_name)
459489
{
460490
NN::MiningProjectMap projects;
@@ -846,6 +876,15 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml)
846876
<external_cpid>f5d8234352e5a5ae3915debba7258294</external_cpid>
847877
</project>
848878
)XML",
879+
// Pool CPID:
880+
R"XML(
881+
<project>
882+
<master_url>https://example.com/</master_url>
883+
<project_name>Project Name 7</project_name>
884+
<team_name>Gridcoin</team_name>
885+
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
886+
</project>
887+
)XML",
849888
}));
850889

851890
NN::Cpid cpid = NN::Cpid::Parse("f5d8234352e5a5ae3915debba7258294");
@@ -854,7 +893,7 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml)
854893
BOOST_CHECK(NN::Researcher::Get()->Id() == NN::MiningId::ForInvestor());
855894

856895
const NN::MiningProjectMap& projects = NN::Researcher::Get()->Projects();
857-
BOOST_CHECK(projects.size() == 6);
896+
BOOST_CHECK(projects.size() == 7);
858897

859898
if (const NN::ProjectOption project1 = projects.Try("project name 1")) {
860899
BOOST_CHECK(project1->m_name == "project name 1");
@@ -916,6 +955,14 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml)
916955
BOOST_FAIL("Project 6 does not exist in the mining project map.");
917956
}
918957

958+
if (const NN::ProjectOption project6 = projects.Try("project name 7")) {
959+
BOOST_CHECK(project6->m_name == "project name 7");
960+
BOOST_CHECK(project6->m_error == NN::MiningProject::Error::POOL);
961+
BOOST_CHECK(project6->Eligible() == false);
962+
} else {
963+
BOOST_FAIL("Project 7 does not exist in the mining project map.");
964+
}
965+
919966
// Clean up:
920967
SetArgument("email", "");
921968
NN::Researcher::Reload(NN::MiningProjectMap());
@@ -1475,4 +1522,85 @@ BOOST_AUTO_TEST_CASE(it_resets_to_investor_mode_when_explicitly_configured,
14751522
NN::Researcher::Reload(NN::MiningProjectMap());
14761523
}
14771524

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

0 commit comments

Comments
 (0)