diff --git a/src/neuralnet/researcher.cpp b/src/neuralnet/researcher.cpp
index b7d5d5f43a..ff065d9b1d 100644
--- a/src/neuralnet/researcher.cpp
+++ b/src/neuralnet/researcher.cpp
@@ -180,6 +180,61 @@ const Project* ResolveWhitelistProject(
return nullptr;
}
+//!
+//! \brief Represents a Gridcoin pool that stakes on behalf of its users.
+//!
+//! The wallet uses these entries to detect when BOINC is attached to a pool
+//! account so that it can provide more useful information in the UI.
+//!
+class MiningPool
+{
+public:
+ MiningPool(const Cpid cpid, std::string m_name, std::string m_url)
+ : m_cpid(cpid), m_name(std::move(m_name)), m_url(std::move(m_url))
+ {
+ }
+
+ MiningPool(const std::string& cpid, std::string m_name, std::string m_url)
+ : MiningPool(Cpid::Parse(cpid), std::move(m_name), std::move(m_url))
+ {
+ }
+
+ Cpid m_cpid; //!< The pool's external CPID.
+ std::string m_name; //!< The name of the pool.
+ std::string m_url; //!< The pool's website URL.
+};
+
+//!
+//! \brief The set of known Gridcoin pools.
+//!
+//! TODO: In the future, we may add a contract type that allows pool operators
+//! to register a pool via the blockchain. The static list gets us by for now.
+//!
+const MiningPool g_pools[] = {
+ { "7d0d73fe026d66fd4ab8d5d8da32a611", "grcpool.com", "https://grcpool.com/" },
+ { "a914eba952be5dfcf73d926b508fd5fa", "grcpool.com-2", "https://grcpool.com/" },
+ { "163f049997e8a2dee054d69a7720bf05", "grcpool.com-3", "https://grcpool.com/" },
+ { "326bb50c0dd0ba9d46e15fae3484af35", "Arikado", "https://gridcoinpool.ru/" },
+};
+
+//!
+//! \brief Determine whether the provided CPID belongs to a Gridcoin pool.
+//!
+//! \param cpid An external CPID for a project loaded from BOINC.
+//!
+//! \return \c true if the CPID matches a known Gridcoin pool's CPID.
+//!
+bool IsPoolCpid(const Cpid cpid)
+{
+ for (const auto& pool : g_pools) {
+ if (pool.m_cpid == cpid) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
//!
//! \brief Fetch the contents of BOINC's client_state.xml file from disk.
//!
@@ -315,6 +370,9 @@ void TryProjectCpid(MiningId& mining_id, const MiningProject& project)
case MiningProject::Error::INVALID_TEAM:
LogPrintf("Project %s's team is not whitelisted.", project.m_name);
return;
+ case MiningProject::Error::POOL:
+ LogPrintf("Project %s is attached to a pool.", project.m_name);
+ return;
}
mining_id = project.m_cpid;
@@ -411,13 +469,16 @@ void StoreResearcher(Researcher context)
case ResearcherStatus::ACTIVE:
msMiningErrors = _("Eligible for Research Rewards");
break;
+ case ResearcherStatus::POOL:
+ msMiningErrors = _("Staking Only - Pool Detected");
+ break;
case ResearcherStatus::NO_PROJECTS:
msMiningErrors = _("Staking Only - No Eligible Research Projects");
break;
case ResearcherStatus::NO_BEACON:
msMiningErrors = _("Staking Only - No active beacon");
break;
- default:
+ case ResearcherStatus::INVESTOR:
msMiningErrors = _("Staking Only - Investor Mode");
break;
}
@@ -770,11 +831,16 @@ MiningProject MiningProject::Parse(const std::string& xml)
ExtractXML(xml, "", ""),
ExtractXML(xml, "", ""));
+ if (IsPoolCpid(project.m_cpid) && !GetBoolArg("-pooloperator", false)) {
+ project.m_error = MiningProject::Error::POOL;
+ return project;
+ }
+
if (project.m_cpid.IsZero()) {
const std::string external_cpid
= ExtractXML(xml, "", "");
- // A bug in BOINC sometimes results in an empty external CPID element
+ // Old BOINC server versions may not provide an external CPID element
// in client_state.xml. For these cases, we'll recompute the external
// CPID of the project from the internal CPID and email address:
//
@@ -833,15 +899,17 @@ std::string MiningProject::ErrorMessage() const
case Error::INVALID_TEAM: return _("Invalid team");
case Error::MALFORMED_CPID: return _("Malformed CPID");
case Error::MISMATCHED_CPID: return _("Project email mismatch");
- default: return _("Unknown error");
+ case Error::POOL: return _("Pool");
}
+
+ return _("Unknown error");
}
// -----------------------------------------------------------------------------
// Class: MiningProjectMap
// -----------------------------------------------------------------------------
-MiningProjectMap::MiningProjectMap()
+MiningProjectMap::MiningProjectMap() : m_has_pool_project(false)
{
}
@@ -883,6 +951,11 @@ bool MiningProjectMap::empty() const
return m_projects.empty();
}
+bool MiningProjectMap::ContainsPool() const
+{
+ return m_has_pool_project;
+}
+
ProjectOption MiningProjectMap::Try(const std::string& name) const
{
const auto iter = m_projects.find(name);
@@ -896,6 +969,7 @@ ProjectOption MiningProjectMap::Try(const std::string& name) const
void MiningProjectMap::Set(MiningProject project)
{
+ m_has_pool_project |= project.m_error == MiningProject::Error::POOL;
m_projects.emplace(project.m_name, std::move(project));
}
@@ -1155,6 +1229,10 @@ ResearcherStatus Researcher::Status() const
}
if (!m_projects.empty()) {
+ if (m_projects.ContainsPool()) {
+ return ResearcherStatus::POOL;
+ }
+
return ResearcherStatus::NO_PROJECTS;
}
diff --git a/src/neuralnet/researcher.h b/src/neuralnet/researcher.h
index 772a44c034..520330d815 100644
--- a/src/neuralnet/researcher.h
+++ b/src/neuralnet/researcher.h
@@ -28,6 +28,7 @@ enum class ResearcherStatus
{
INVESTOR, //!< BOINC not present; ineligible for research rewards.
ACTIVE, //!< CPID eligible for research rewards.
+ POOL, //!< BOINC attached to projects for a Gridcoin mining pool.
NO_PROJECTS, //!< BOINC present, but no eligible projects (investor).
NO_BEACON, //!< No active beacon public key advertised.
};
@@ -50,6 +51,7 @@ struct MiningProject
INVALID_TEAM, //!< Project not joined to a whitelisted team.
MALFORMED_CPID, //!< Failed to parse a valid external CPID.
MISMATCHED_CPID, //!< External CPID failed internal CPID + email test.
+ POOL, //!< External CPID matches a Gridcoin pool.
};
//!
@@ -172,6 +174,13 @@ class MiningProjectMap
//!
bool empty() const;
+ //!
+ //! \brief Determine whether the map contains a project attached to a pool.
+ //!
+ //! \return \c true if a project in the map has a pool CPID.
+ //!
+ bool ContainsPool() const;
+
//!
//! \brief Try to get the loaded BOINC project with the specified name.
//!
@@ -206,6 +215,11 @@ class MiningProjectMap
//! \brief Stores the local BOINC projects loaded from client_state.xml.
//!
ProjectStorage m_projects;
+
+ //!
+ //! \brief Caches whether the map contains a project attached to a pool.
+ //!
+ bool m_has_pool_project;
}; // MiningProjectMap
class Researcher; // forward for ResearcherPtr
diff --git a/src/test/neuralnet/researcher_tests.cpp b/src/test/neuralnet/researcher_tests.cpp
index 5f5dc98440..757e7e4650 100644
--- a/src/test/neuralnet/researcher_tests.cpp
+++ b/src/test/neuralnet/researcher_tests.cpp
@@ -249,6 +249,25 @@ BOOST_AUTO_TEST_CASE(it_determines_whether_a_project_is_eligible)
BOOST_CHECK(project.Eligible() == false);
}
+BOOST_AUTO_TEST_CASE(it_detects_projects_with_pool_cpids)
+{
+ // The XML string contains a subset of data found within a element
+ // from BOINC's client_state.xml file:
+ //
+ NN::MiningProject project = NN::MiningProject::Parse(
+ R"XML(
+
+ https://example.com/
+ Project Name
+ Team Name
+ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ 7d0d73fe026d66fd4ab8d5d8da32a611
+
+ )XML");
+
+ BOOST_CHECK(project.m_error == NN::MiningProject::Error::POOL);
+}
+
BOOST_AUTO_TEST_CASE(it_determines_whether_a_project_is_whitelisted)
{
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)
BOOST_CHECK(projects.empty() == false);
}
+BOOST_AUTO_TEST_CASE(it_indicates_whether_it_contains_any_pool_projects)
+{
+ NN::MiningProjectMap projects;
+ NN::MiningProject project("project name", NN::Cpid(), "team name", "url");
+
+ project.m_error = NN::MiningProject::Error::POOL;
+ projects.Set(std::move(project));
+
+ BOOST_CHECK(projects.ContainsPool() == true);
+}
+
BOOST_AUTO_TEST_CASE(it_fetches_a_project_by_name)
{
NN::MiningProjectMap projects;
@@ -846,6 +876,15 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml)
f5d8234352e5a5ae3915debba7258294
)XML",
+ // Pool CPID:
+ R"XML(
+
+ https://example.com/
+ Project Name 7
+ Gridcoin
+ 7d0d73fe026d66fd4ab8d5d8da32a611
+
+ )XML",
}));
NN::Cpid cpid = NN::Cpid::Parse("f5d8234352e5a5ae3915debba7258294");
@@ -854,7 +893,7 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml)
BOOST_CHECK(NN::Researcher::Get()->Id() == NN::MiningId::ForInvestor());
const NN::MiningProjectMap& projects = NN::Researcher::Get()->Projects();
- BOOST_CHECK(projects.size() == 6);
+ BOOST_CHECK(projects.size() == 7);
if (const NN::ProjectOption project1 = projects.Try("project name 1")) {
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)
BOOST_FAIL("Project 6 does not exist in the mining project map.");
}
+ if (const NN::ProjectOption project6 = projects.Try("project name 7")) {
+ BOOST_CHECK(project6->m_name == "project name 7");
+ BOOST_CHECK(project6->m_error == NN::MiningProject::Error::POOL);
+ BOOST_CHECK(project6->Eligible() == false);
+ } else {
+ BOOST_FAIL("Project 7 does not exist in the mining project map.");
+ }
+
// Clean up:
SetArgument("email", "");
NN::Researcher::Reload(NN::MiningProjectMap());
@@ -1475,4 +1522,85 @@ BOOST_AUTO_TEST_CASE(it_resets_to_investor_mode_when_explicitly_configured,
NN::Researcher::Reload(NN::MiningProjectMap());
}
+BOOST_AUTO_TEST_CASE(it_resets_to_investor_when_it_only_finds_pool_projects)
+{
+ const NN::Cpid cpid = NN::Cpid::Parse("f5d8234352e5a5ae3915debba7258294");
+ SetArgument("email", "researcher@example.com");
+ AddTestBeacon(cpid);
+
+ // External CPID is a pool CPID:
+ NN::Researcher::Reload(NN::MiningProjectMap::Parse({
+ R"XML(
+
+ https://example.com/
+ Pool Project
+ Gridcoin
+ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ 7d0d73fe026d66fd4ab8d5d8da32a611
+
+ )XML",
+ }));
+
+ BOOST_CHECK(NN::Researcher::Get()->Id() == NN::MiningId::ForInvestor());
+ BOOST_CHECK(NN::Researcher::Get()->Eligible() == false);
+ BOOST_CHECK(NN::Researcher::Get()->Status() == NN::ResearcherStatus::POOL);
+
+ NN::Researcher::Reload(NN::MiningProjectMap::Parse({
+ R"XML(
+
+ https://example.com/
+ My Project
+ Gridcoin
+ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ 7d0d73fe026d66fd4ab8d5d8da32a611
+
+ )XML",
+ R"XML(
+
+ https://example.com/
+ Pool Project
+ Gridcoin
+ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ f5d8234352e5a5ae3915debba7258294
+
+ )XML",
+ }));
+
+ BOOST_CHECK(NN::Researcher::Get()->Id() == cpid);
+ BOOST_CHECK(NN::Researcher::Get()->Eligible() == true);
+ BOOST_CHECK(NN::Researcher::Get()->Status() != NN::ResearcherStatus::POOL);
+
+ // Clean up:
+ SetArgument("email", "");
+ RemoveTestBeacon(cpid);
+ NN::Researcher::Reload(NN::MiningProjectMap());
+}
+
+BOOST_AUTO_TEST_CASE(it_allows_pool_operators_to_load_pool_cpids)
+{
+ SetArgument("pooloperator", "1");
+
+ // External CPID is a pool CPID:
+ NN::Researcher::Reload(NN::MiningProjectMap::Parse({
+ R"XML(
+
+ https://example.com/
+ Name
+ Gridcoin
+ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ 7d0d73fe026d66fd4ab8d5d8da32a611
+
+ )XML",
+ }));
+
+ // We can't completely test that a pool CPID loads, but we can check that
+ // the it didn't fail because of the pool CPID:
+ //
+ BOOST_CHECK(NN::Researcher::Get()->Status() != NN::ResearcherStatus::POOL);
+
+ // Clean up:
+ SetArgument("pooloperator", "0");
+ NN::Researcher::Reload(NN::MiningProjectMap());
+}
+
BOOST_AUTO_TEST_SUITE_END()