Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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 <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 @@ -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;
Expand Down Expand Up @@ -846,6 +876,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 @@ -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");
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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", "[email protected]");
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()