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()