Skip to content

Commit

Permalink
Merge pull request #1775 from cyrossignol/detect-pools
Browse files Browse the repository at this point in the history
gui: Add context for when BOINC is attached to a pool
  • Loading branch information
jamescowens committed Jul 6, 2020
2 parents a794d75 + 1fdc0df commit 24ea76b
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 5 deletions.
86 changes: 82 additions & 4 deletions src/neuralnet/researcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//!
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -770,11 +831,16 @@ MiningProject MiningProject::Parse(const std::string& xml)
ExtractXML(xml, "<team_name>", "</team_name>"),
ExtractXML(xml, "<master_url>", "</master_url>"));

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, "<external_cpid>", "</external_cpid>");

// 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:
//
Expand Down Expand Up @@ -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)
{
}

Expand Down Expand Up @@ -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);
Expand All @@ -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));
}

Expand Down Expand Up @@ -1155,6 +1229,10 @@ ResearcherStatus Researcher::Status() const
}

if (!m_projects.empty()) {
if (m_projects.ContainsPool()) {
return ResearcherStatus::POOL;
}

return ResearcherStatus::NO_PROJECTS;
}

Expand Down
14 changes: 14 additions & 0 deletions src/neuralnet/researcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
};
Expand All @@ -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.
};

//!
Expand Down Expand Up @@ -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.
//!
Expand Down Expand Up @@ -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
Expand Down
130 changes: 129 additions & 1 deletion src/test/neuralnet/researcher_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,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 <project> element
// from BOINC's client_state.xml file:
//
NN::MiningProject project = NN::MiningProject::Parse(
R"XML(
<project>
<master_url>https://example.com/</master_url>
<project_name>Project Name</project_name>
<team_name>Team Name</team_name>
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
</project>
)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");
Expand Down Expand Up @@ -464,6 +483,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;
Expand Down Expand Up @@ -855,6 +885,15 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml)
<external_cpid>f5d8234352e5a5ae3915debba7258294</external_cpid>
</project>
)XML",
// Pool CPID:
R"XML(
<project>
<master_url>https://example.com/</master_url>
<project_name>Project Name 7</project_name>
<team_name>Gridcoin</team_name>
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
</project>
)XML",
}));

NN::Cpid cpid = NN::Cpid::Parse("f5d8234352e5a5ae3915debba7258294");
Expand All @@ -863,7 +902,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");
Expand Down Expand Up @@ -925,6 +964,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());
Expand Down Expand Up @@ -1484,4 +1531,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(
<project>
<master_url>https://example.com/</master_url>
<project_name>Pool Project</project_name>
<team_name>Gridcoin</team_name>
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
</project>
)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(
<project>
<master_url>https://example.com/</master_url>
<project_name>My Project</project_name>
<team_name>Gridcoin</team_name>
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
</project>
)XML",
R"XML(
<project>
<master_url>https://example.com/</master_url>
<project_name>Pool Project</project_name>
<team_name>Gridcoin</team_name>
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
<external_cpid>f5d8234352e5a5ae3915debba7258294</external_cpid>
</project>
)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(
<project>
<master_url>https://example.com/</master_url>
<project_name>Name</project_name>
<team_name>Gridcoin</team_name>
<cross_project_id>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</cross_project_id>
<external_cpid>7d0d73fe026d66fd4ab8d5d8da32a611</external_cpid>
</project>
)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()

0 comments on commit 24ea76b

Please sign in to comment.