Browse Source

Half-finished Miner/Farm framework.

cl-refactor
Gav Wood 10 years ago
parent
commit
87bb1195a5
  1. 16
      libdevcore/Worker.cpp
  2. 6
      libdevcore/Worker.h
  3. 12
      libethcore/Miner.cpp
  4. 146
      libethcore/Miner.h
  5. 56
      libethcore/ProofOfWork.cpp
  6. 124
      libethcore/ProofOfWork.h
  7. 2
      libethereum/Client.cpp
  8. 4
      libethereum/Client.h
  9. 12
      libethereum/Farm.cpp
  10. 153
      libethereum/Farm.h
  11. 2
      libethereum/Miner.cpp
  12. 97
      libethereum/Miner.h
  13. 5
      libethereum/TransactionQueue.cpp
  14. 8
      libethereum/TransactionQueue.h
  15. 2
      libethereumx/Ethereum.h

16
libdevcore/Worker.cpp

@ -39,12 +39,7 @@ void Worker::startWorking()
{
setThreadName(m_name.c_str());
startedWorking();
while (!m_stop)
{
if (m_idleWaitMs)
this_thread::sleep_for(chrono::milliseconds(m_idleWaitMs));
doWork();
}
workLoop();
cnote << "Finishing up worker thread";
doneWorking();
}));
@ -63,3 +58,12 @@ void Worker::stopWorking()
cnote << "Stopped" << m_name;
}
void Worker::workLoop()
{
while (!m_stop)
{
if (m_idleWaitMs)
this_thread::sleep_for(chrono::milliseconds(m_idleWaitMs));
doWork();
}
}

6
libdevcore/Worker.h

@ -57,7 +57,11 @@ protected:
virtual void startedWorking() {}
/// Called continuously following sleep for m_idleWaitMs.
virtual void doWork() = 0;
virtual void doWork() {}
/// Overrides doWork(); should call shouldStop() often and exit when true.
virtual void workLoop();
bool shouldStop() const { return m_stop; }
/// Called when is to be stopped, just prior to thread being joined.
virtual void doneWorking() {}

12
libethcore/Miner.cpp

@ -0,0 +1,12 @@
#include "Miner.h"
Miner::Miner()
{
}
Miner::~Miner()
{
}

146
libethcore/Miner.h

@ -0,0 +1,146 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file Miner.h
* @author Alex Leverington <nessence@gmail.com>
* @author Gav Wood <i@gavwood.com>
* @date 2014
*/
#pragma once
#include <thread>
#include <list>
#include <atomic>
#include <libdevcore/Common.h>
#include <libdevcore/Worker.h>
#include <libethcore/Common.h>
#include "State.h"
namespace dev
{
namespace eth
{
struct WorkPackage
{
h256 boundary;
h256 headerHash; ///< When h256() means "pause until notified a new work package is available".
h256 seedHash;
};
static const WorkPackage NullWorkPackage;
/**
* @brief Describes the progress of a mining operation.
*/
struct MiningProgress
{
void combine(MiningProgress const& _m) { requirement = std::max(requirement, _m.requirement); best = std::min(best, _m.best); current = std::max(current, _m.current); hashes += _m.hashes; ms = std::max(ms, _m.ms); }
double requirement = 0; ///< The PoW requirement - as the second logarithm of the minimum acceptable hash.
double best = 1e99; ///< The PoW achievement - as the second logarithm of the minimum found hash.
double current = 0; ///< The most recent PoW achievement - as the second logarithm of the presently found hash.
unsigned hashes = 0; ///< Total number of hashes computed.
unsigned ms = 0; ///< Total number of milliseconds of mining thus far.
};
/**
* @brief Class for hosting one or more Miners.
* @warning Must be implemented in a threadsafe manner since it will be called from multiple
* miner threads.
*/
class FarmFace
{
public:
/**
* @brief Called from a Miner to note a WorkPackage has a solution.
* @param _p The solution.
* @param _wp The WorkPackage that the Solution is for.
* @return true iff the solution was good (implying that mining should be .
*/
virtual bool submitProof(ProofOfWork::Solution const& _p, WorkPackage const& _wp) = 0;
};
/**
* @brief A miner - a member and adoptee of the Farm.
*/
class Miner
{
public:
using ConstructionInfo = std::pair<FarmFace*, unsigned>;
Miner(ConstructionInfo const& _ci):
m_farm(_ci.first),
m_index(_ci.second)
{}
// API FOR THE FARM TO CALL IN WITH
void setWork(WorkPackage const& _work = WorkPackage())
{
Guard l(x_work);
if (_work.headerHash != h256())
kickOff(m_work);
else if (m_work.headerHash == h256() && _work.headerHash != h256())
pause();
m_work = _work;
}
unsigned index() const { return m_index; }
protected:
// REQUIRED TO BE REIMPLEMENTED BY A SUBCLASS:
/**
* @brief Begin working on a given work package, discarding any previous work.
* @param _work The package for which to find a solution.
*/
virtual void kickOff(WorkPackage const& _work) = 0;
/**
* @brief No work left to be done. Pause until told to kickOff().
*/
virtual void pause() = 0;
// AVAILABLE FOR A SUBCLASS TO CALL:
/**
* @brief Notes that the Miner found a solution.
* @param _s The solution.
* @return true if the solution was correct and that the miner should pause.
*/
bool submitProof(ProofOfWork::Solution const& _s)
{
if (m_farm)
{
Guard l(x_work);
return m_farm->submitProof(_s, m_work, this);
}
return true;
}
private:
FarmFace* m_farm = nullptr;
unsigned m_index;
Mutex x_work;
WorkPackage m_work;
};
}
}

56
libethcore/ProofOfWork.cpp

@ -45,13 +45,38 @@ namespace dev
namespace eth
{
bool EthashPoW::verify(BlockInfo const& _header)
void Ethash::CPUMiner::workLoop()
{
return Ethasher::verify(_header);
}
Solution solution;
class Miner
{
public:
Miner(BlockInfo const& _header):
m_headerHash(_header.headerHash(WithoutNonce)),
m_params(Ethasher::params(_header)),
m_datasetPointer(Ethasher::get()->full(_header).data())
{}
inline h256 mine(uint64_t _nonce)
{
ethash_compute_full(&m_ethashReturn, m_datasetPointer, &m_params, m_headerHash.data(), _nonce);
// cdebug << "Ethasher::mine hh:" << m_headerHash << "nonce:" << (Nonce)(u64)_nonce << " => " << h256(m_ethashReturn.result, h256::ConstructFromPointer);
return h256(m_ethashReturn.result, h256::ConstructFromPointer);
}
inline h256 lastMixHash() const
{
return h256(m_ethashReturn.mix_hash, h256::ConstructFromPointer);
}
private:
ethash_return_value m_ethashReturn;
h256 m_headerHash;
ethash_params m_params;
void const* m_datasetPointer;
};
std::pair<MineInfo, EthashCPU::Solution> EthashCPU::mine(BlockInfo const& _header, unsigned _msTimeout, bool _continue)
{
Ethasher::Miner m(_header);
std::pair<MineInfo, Solution> ret;
@ -70,34 +95,21 @@ std::pair<MineInfo, EthashCPU::Solution> EthashCPU::mine(BlockInfo const& _heade
double best = 1e99; // high enough to be effectively infinity :)
Solution result;
unsigned hashCount = 0;
for (; (std::chrono::steady_clock::now() - startTime) < std::chrono::milliseconds(_msTimeout) && _continue; tryNonce++, hashCount++)
for (; !shouldStop(); tryNonce++, hashCount++)
{
h256 val(m.mine(tryNonce));
best = std::min<double>(best, log2((double)(u256)val));
if (val <= boundary)
{
ret.first.completed = true;
assert(Ethasher::eval(_header, (Nonce)(u64)tryNonce).value == val);
result.mixHash = m.lastMixHash();
result.nonce = u64(tryNonce);
BlockInfo test = _header;
assignResult(result, test);
assert(verify(test));
break;
if (submitProof(solution))
return;
}
}
ret.first.hashes = hashCount;
ret.first.best = best;
ret.second = result;
if (ret.first.completed)
{
BlockInfo test = _header;
assignResult(result, test);
assert(verify(test));
}
return ret;
return;
}
#if ETH_ETHASHCL || !ETH_TRUE

124
libethcore/ProofOfWork.h

@ -29,6 +29,7 @@
#include <libdevcrypto/SHA3.h>
#include "Common.h"
#include "BlockInfo.h"
#include "Miner.h"
#define FAKE_DAGGER 1
@ -51,39 +52,56 @@ struct MineInfo
bool completed = false;
};
class EthashPoW
class EthashCLHook;
class Ethash
{
public:
struct Solution
{
struct Solution
{
Nonce nonce;
h256 mixHash;
};
static bool verify(BlockInfo const& _header);
static void assignResult(Solution const& _r, BlockInfo& _header) { _header.nonce = _r.nonce; _header.mixHash = _r.mixHash; }
virtual unsigned defaultTimeout() const { return 100; }
virtual std::pair<MineInfo, Solution> mine(BlockInfo const& _header, unsigned _msTimeout = 100, bool _continue = true) = 0;
};
class EthashCPU: public EthashPoW
static bool verify(BlockInfo const& _header);
static void assignResult(Solution const& _r, BlockInfo& _header) { _header.nonce = _r.nonce; _header.mixHash = _r.mixHash; }
class CPUMiner: public Miner, Worker
{
public:
std::pair<MineInfo, Solution> mine(BlockInfo const& _header, unsigned _msTimeout = 100, bool _continue = true) override;
CPUMiner(ConstructionInfo const& _ci): Miner(_ci), Worker("miner" + toString(index())) {}
protected:
Nonce m_last;
static unsigned instances() { return thread::hardware_concurrency(); }
void kickOff(WorkPackage const& _work) override
{
stopWorking();
m_work = _work;
startWorking();
}
void pause() override { stopWorking(); }
private:
void workLoop() override;
WorkPackage m_work;
MineInfo m_info;
};
#if ETH_ETHASHCL || !ETH_TRUE
class EthashCLHook;
class EthashCL: public EthashPoW
class GPUMiner: public NewMiner
{
public:
EthashCL();
~EthashCL();
GPUMiner(ConstructionInfo const& _ci): NewMiner(_ci)
{
}
static unsigned instances() { return 1; }
std::pair<MineInfo, Solution> mine(BlockInfo const& _header, unsigned _msTimeout = 100, bool _continue = true) override;
unsigned defaultTimeout() const override { return 500; }
@ -96,81 +114,15 @@ protected:
std::unique_ptr<EthashCLHook> m_hook;
};
using Ethash = EthashCL;
#else
using Ethash = EthashCPU;
#endif
template <class Evaluator>
class ProofOfWorkEngine: public Evaluator
{
public:
using Solution = Nonce;
static bool verify(BlockInfo const& _header) { return (bigint)(u256)Evaluator::eval(_header.headerHash(WithoutNonce), _header.nonce) <= (bigint(1) << 256) / _header.difficulty; }
inline std::pair<MineInfo, Solution> mine(BlockInfo const& _header, unsigned _msTimeout = 100, bool _continue = true);
static void assignResult(Solution const& _r, BlockInfo& _header) { _header.nonce = _r; }
unsigned defaultTimeout() const { return 100; }
using GPUMiner = CPUMiner;
protected:
Nonce m_last;
};
#endif
class SHA3Evaluator
{
public:
static h256 eval(h256 const& _root, Nonce const& _nonce) { h256 b[2] = { _root, h256(_nonce) }; return sha3(bytesConstRef((byte const*)&b[0], 64)); }
};
using SHA3ProofOfWork = ProofOfWorkEngine<SHA3Evaluator>;
using ProofOfWork = Ethash;
template <class Evaluator>
std::pair<MineInfo, typename ProofOfWorkEngine<Evaluator>::Solution> ProofOfWorkEngine<Evaluator>::mine(BlockInfo const& _header, unsigned _msTimeout, bool _continue)
{
auto headerHashWithoutNonce = _header.headerHash(WithoutNonce);
auto difficulty = _header.difficulty;
std::pair<MineInfo, Nonce> ret;
static std::mt19937_64 s_eng((time(0) + *reinterpret_cast<unsigned*>(m_last.data())));
Nonce::Arith s = (m_last = Nonce::random(s_eng));
bigint d = (bigint(1) << 256) / difficulty;
ret.first.requirement = log2((double)d);
// 2^ 0 32 64 128 256
// [--------*-------------------------]
//
// evaluate until we run out of time
auto startTime = std::chrono::steady_clock::now();
double best = 1e99; // high enough to be effectively infinity :)
ProofOfWorkEngine<Evaluator>::Solution solution;
unsigned h = 0;
for (; (std::chrono::steady_clock::now() - startTime) < std::chrono::milliseconds(_msTimeout) && _continue; s++, h++)
{
solution = (ProofOfWorkEngine<Evaluator>::Solution)s;
auto e = (bigint)(u256)Evaluator::eval(headerHashWithoutNonce, solution);
best = std::min<double>(best, log2((double)e));
if (e <= d)
{
ret.first.completed = true;
break;
}
}
ret.first.hashes = h;
ret.first.best = best;
ret.second = solution;
if (ret.first.completed)
{
BlockInfo test = _header;
assignResult(solution, test);
assert(verify(test));
}
return ret;
}
}
}

2
libethereum/Client.cpp

@ -467,7 +467,7 @@ void Client::doWork()
cworkin << "WORK";
h256Set changeds;
auto maintainMiner = [&](Miner& m)
auto maintainMiner = [&](OldMiner& m)
{
if (m.isComplete())
{

4
libethereum/Client.h

@ -72,7 +72,7 @@ private:
std::string m_path;
};
class RemoteMiner: public Miner
class RemoteMiner: public OldMiner
{
public:
RemoteMiner() {}
@ -124,7 +124,7 @@ struct ClientDetail: public LogChannel { static const char* name() { return " C
*/
class Client: public MinerHost, public ClientBase, Worker
{
friend class Miner;
friend class OldMiner;
public:
/// New-style Constructor.

12
libethereum/Farm.cpp

@ -0,0 +1,12 @@
#include "Farm.h"
Farm::Farm()
{
}
Farm::~Farm()
{
}

153
libethereum/Farm.h

@ -0,0 +1,153 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file Miner.h
* @author Alex Leverington <nessence@gmail.com>
* @author Gav Wood <i@gavwood.com>
* @date 2014
*/
#pragma once
#include <thread>
#include <list>
#include <atomic>
#include <libdevcore/Common.h>
#include <libdevcore/Worker.h>
#include <libethcore/Common.h>
#include "Miner.h"
namespace dev
{
namespace eth
{
/**
* @brief A collective of Miners.
* Miners ask for work, then submit proofs
* @threadsafe
*/
template <class ProofOfWork>
class Farm: public FarmFace
{
public:
/**
* @brief Sets the current mining mission.
* @param _bi The block (header) we wish to be mining.
*/
void setWork(BlockInfo const& _bi)
{
WriteGuard l(x_work);
m_header = _bi;
m_work.boundary = _bi.boundary();
m_work.headerHash = _bi.headerHash(WithNonce);
m_work.seedHash = _bi.seedHash();
ReadGuard l(x_miners);
for (auto const& m: miners)
m->setWork(m_work);
}
/**
* @brief (Re)start miners for CPU only.
* @returns true if started properly.
*/
bool startCPU() { return start<ProofOfWork::CPUMiner>(); }
/**
* @brief (Re)start miners for GPU only.
* @returns true if started properly.
*/
bool startGPU() { start<ProofOfWork::GPUMiner>(); }
/**
* @brief Stop all mining activities.
*/
void stop()
{
WriteGuard l(x_miners);
m_miners.clear();
}
/**
* @brief Get information on the progress of mining this work package.
* @return The progress with mining so far.
*/
MineProgress const& mineProgress() const { ReadGuard l(x_progress); return m_progress; }
protected:
// TO BE REIMPLEMENTED BY THE SUBCLASS
/**
* @brief Provides a valid header based upon that received previously with setWork().
* @param _bi The now-valid header.
* @return true if the header was good and that the Farm should pause until more work is submitted.
*/
virtual bool submitHeader(BlockInfo const& _bi) = 0;
private:
/**
* @brief Called from a Miner to note a WorkPackage has a solution.
* @param _p The solution.
* @param _wp The WorkPackage that the Solution is for.
* @return true iff the solution was good (implying that mining should be .
*/
bool submitProof(ProofOfWork::Solution const& _p, WorkPackage const& _wp, NewMiner* _m) override
{
if (_wp.headerHash != m_work.headerHash)
return false;
ProofOfWork::assignResult(_p, m_header);
if (submitHeader(m_header))
{
ReadGuard l(x_miners);
for (std::shared_ptr<NewMiner> const& m: m_miners)
if (m.get() != _m)
m->pause();
m_work.headerHash = h256();
return true;
}
return false;
}
/**
* @brief Start a number of miners.
*/
template <class MinerType>
bool start()
{
WriteGuard l(x_miners);
if (!m_miners.empty() && !!std::dynamic_pointer_cast<MinerType>(m_miners[0]))
return true;
m_miners.clear();
m_miners.reserve(MinerType::instances());
for (unsigned i = 0; i < MinerType::instances(); ++i)
m_miners.push_back(new MinerType(std::make_pair(this, i)));
return true;
}
mutable SharedMutex x_miners;
std::vector<std::shared_ptr<NewMiner>> m_miners;
mutable SharedMutex x_progress;
MineProgress m_progress;
mutable SharedMutex x_work;
WorkPackage m_work;
BlockInfo m_header;
};
}
}

2
libethereum/Miner.cpp

@ -28,7 +28,7 @@ using namespace std;
using namespace dev;
using namespace dev::eth;
Miner::~Miner() {}
OldMiner::~OldMiner() {}
LocalMiner::LocalMiner(MinerHost* _host, unsigned _id)
{

97
libethereum/Miner.h

@ -28,6 +28,7 @@
#include <libdevcore/Common.h>
#include <libdevcore/Worker.h>
#include <libethcore/Common.h>
#include <libethcore/Miner.h>
#include "State.h"
namespace dev
@ -36,28 +37,6 @@ namespace dev
namespace eth
{
struct WorkPackage
{
h256 boundary;
h256 headerHash; ///< When h256() means "pause until notified a new work package is available".
h256 seedHash;
};
static const WorkPackage NullWorkPackage;
/**
* @brief Describes the progress of a mining operation.
*/
struct MineProgress
{
void combine(MineProgress const& _m) { requirement = std::max(requirement, _m.requirement); best = std::min(best, _m.best); current = std::max(current, _m.current); hashes += _m.hashes; ms = std::max(ms, _m.ms); }
double requirement = 0; ///< The PoW requirement - as the second logarithm of the minimum acceptable hash.
double best = 1e99; ///< The PoW achievement - as the second logarithm of the minimum found hash.
double current = 0; ///< The most recent PoW achievement - as the second logarithm of the presently found hash.
unsigned hashes = 0; ///< Total number of hashes computed.
unsigned ms = 0; ///< Total number of milliseconds of mining thus far.
};
/**
* @brief Class for hosting one or more Miners.
* @warning Must be implemented in a threadsafe manner since it will be called from multiple
@ -66,10 +45,6 @@ struct MineProgress
class MinerHost
{
public:
// ============================= NEW API =============================
virtual WorkPackage const& getWork() const { return NullWorkPackage; }
// ============================= OLD API =============================
virtual void setupState(State& _s) = 0; ///< Reset the given State object to the one that should be being mined.
virtual void onProgressed() {} ///< Called once some progress has been made.
virtual void onComplete() {} ///< Called once a block is found.
@ -77,17 +52,17 @@ public:
virtual bool turbo() const = 0; ///< @returns true iff the Miner should use GPU if possible.
};
class Miner
class OldMiner
{
public:
virtual ~Miner();
virtual ~OldMiner();
virtual void noteStateChange() = 0;
virtual bool isComplete() const = 0;
virtual bytes const& blockData() const = 0;
};
class AsyncMiner: public Miner
class AsyncMiner: public OldMiner
{
public:
/// Null constructor.
@ -187,69 +162,5 @@ private:
std::list<MineInfo> m_mineHistory; ///< What the history of our mining?
};
/**
* @brief A collective of Miners.
* Miners ask for work, then submit proofs
* @threadsafe
*/
class Farm: public MinerHost
{
public:
/**
* @brief Sets the current mining mission.
* @param _bi The block (header) we wish to be mining.
*/
void setWork(BlockInfo const& _bi);
/**
* @brief (Re)start miners for CPU only.
* @returns true if started properly.
*/
bool startCPU();
/**
* @brief (Re)start miners for GPU only.
* @returns true if started properly.
*/
bool startGPU();
/**
* @brief Stop all mining activities.
*/
void stop();
/**
* @brief Get information on the progress of mining this work package.
* @return The progress with mining so far.
*/
MineProgress const& mineProgress() const { ReadGuard l(x_progress); return m_progress; }
protected:
/**
* @brief Called by a Miner to retrieve a work package. Reimplemented from MinerHost.
* @return The work package to solve.
*/
virtual WorkPackage const& getWork() const override { ReadGuard l(x_work); return m_work; }
/**
* @brief Called from a Miner to note a WorkPackage has a solution.
* @param _p The solution.
* @param _wp The WorkPackage that the Solution is for.
* @return true iff the solution was good (implying that mining should be .
*/
virtual bool submitProof(ProofOfWork::Solution const& _p, WorkPackage const& _wp) = 0;
private:
mutable SharedMutex x_miners;
std::vector<std::shared_ptr<Miner>> m_miners;
mutable SharedMutex x_progress;
MineProgress m_progress;
mutable SharedMutex x_work;
WorkPackage m_work;
};
}
}

5
libethereum/TransactionQueue.cpp

@ -28,7 +28,7 @@ using namespace std;
using namespace dev;
using namespace dev::eth;
ImportResult TransactionQueue::import(bytesConstRef _transactionRLP)
ImportResult TransactionQueue::import(bytesConstRef _transactionRLP, ImportCallback const& _cb)
{
// Check if we already know this transaction.
h256 h = sha3(_transactionRLP);
@ -50,7 +50,8 @@ ImportResult TransactionQueue::import(bytesConstRef _transactionRLP)
// If valid, append to blocks.
m_current[h] = t;
m_known.insert(h);
if (_cb)
m_callbacks[h] = _cb;
ctxq << "Queued vaguely legit-looking transaction" << h.abridged();
}
catch (Exception const& _e)

8
libethereum/TransactionQueue.h

@ -21,6 +21,7 @@
#pragma once
#include <functional>
#include <boost/thread.hpp>
#include <libdevcore/Common.h>
#include <libdevcore/Guards.h>
@ -45,8 +46,10 @@ struct TransactionQueueChannel: public LogChannel { static const char* name() {
class TransactionQueue
{
public:
ImportResult import(bytes const& _tx) { return import(&_tx); }
ImportResult import(bytesConstRef _tx);
using ImportCallback = std::function<void(ImportResult)>;
ImportResult import(bytes const& _tx, ImportCallback const& _cb = ImportCallback()) { return import(&_tx); }
ImportResult import(bytesConstRef _tx, ImportCallback const& _cb = ImportCallback());
void drop(h256 _txHash);
@ -63,6 +66,7 @@ private:
std::set<h256> m_known; ///< Hashes of transactions in both sets.
std::map<h256, Transaction> m_current; ///< Map of SHA3(tx) to tx.
std::multimap<Address, std::pair<h256, Transaction>> m_unknown; ///< For transactions that have a future nonce; we map their sender address to the tx stuff, and insert once the sender has a valid TX.
std::map<h256, std::function<void(ImportResult)>> m_callbacks; ///< Called once
};
}

2
libethereumx/Ethereum.h

@ -52,7 +52,7 @@ class Client;
*/
class Ethereum
{
friend class Miner;
friend class OldMiner;
public:
/// Constructor. After this, everything should be set up to go.

Loading…
Cancel
Save