概述:
区块链:本质是分布式账本,账本分布在全球各处,很难被篡改
以太坊Eth:Eth是区块链技术中的一种,是区块链中的二师兄(大哥是比特币)
区块链的金融属性:围绕不能被篡改有公信力的账本,建立线上交易系统。以太坊为例,可以在以太坊账本上进行交易的以太币已发行超过1.1亿个,每个币当前市价2.7w人民币,以太坊市值2.9万亿,这些币可以进行金融交易;交易需求刺激技术投入
区块链技术:包括了交易程序,账本程序,矿池程序,挖矿程序(账本加密打包);
矿池:矿池程序,工作原理类似包工头,接收记账任务,分发给很多矿工一起完成。矿池有其存在的理由,全网算力激增,个人矿工难以完成任务,所以有矿池的出现。
矿工:工作单纯,通常从矿池接收任务,经过大量固定流程的计算,上报结果。
Ethminer:就是以太坊开源的矿工程序,负责接收计算任务,计算结果上报,获得报酬。
综上所述,区块链技术包含了多个环节的技术。本文从主要从Ethminer的代码入手,对矿工程序进行分析。
Stratum以太坊通讯协议:
矿工从矿池接收和上报任务,都要依赖通讯协议,以太坊常用的是Stratum协议。
Stratum是在tcp协议之上封装,以Jason为格式的协议,可以选择是否用SSL进行加密。
Stratum协议主要格式:
请求:
{
"id": 通讯ID
"method": 方法名称,
"params": [
方法所带参数
]
}
回复:
{
"id": ID,和请求ID对应
"result": 回复的信息都放在result里
"error": 请求是否出错
}
?方法:订阅
client请求:
{
"id":1,
"method":"eth_submitLogin",
"worker":"xyz-test2",
"params":["0xcBFC5Cb9b315Cd82685c0Ba17A82db872Be66b01","x"],
"jsonrpc":"2.0"
}
?server回复:
{
"id":1,
"jsonrpc":"2.0",
"result":true
}
worker: 是矿工的名字
params中的一串40位16进制数,是钱包地址
有些矿工的上报格式也有差异,有的worker字段为空,名字跟在钱包地址后,以点做分割如:
???0xcBFC5Cb9b315Cd82685c0Ba17A82db872Be66b01.?xyz-test2
注意,这里client请求和server的回复id都一致,client的id会递增,在client发起请求的过程都有这个规律
方法:请求任务
client发送请求:
{"id":2,"method":"eth_getWork","params":[],"jsonrpc":"2.0"}
发送用户名和密码
sever回复:
{"id":2,"jsonrpc":"2.0","result":{"status":"ok"}}
client订阅成功后,就会发起请求任务的消息
server只要简洁的回复ok后,就可以开始下放计算任务
方法:下发任务
{
"id":0,
"jsonrpc":"2.0",
"result":[
"0xe41d8d6a87364b010c92c9a4ef44965ee801faf64b90dc9b40f71ff74eea3e02", #headerhash
"0x0ae1dc880080eca07b1a976949eb91b4f67fa533a59767f24c2e1d332182a024", #seedhash
"0x00000000ffff00000000ffff00000000ffff00000000ffff00000000ffff0000", #boundary
"0xd1d351" #代表block编号
]
}
接到矿工请求任务的信息后,矿池就每隔1到2秒,下发一次计算任务,计算任务的ID都为0;
每个字段的含义如代码上注释;这部分信息就是计算所需输入;
headerhash就是每个区块的头部hash值,每个任务都不相同
seedhash这个值用于计算DAG图,每纪元(epoch大概100小时左右)更新一次,用相同的seedhash计算出来的DAG图都是相同的
boundary代表难度限制,boundary越小难度越高,计算结果要小于boundary
block编号,hashhead对应的区块编号,这个信息在计算的时候没有用处
方法:提交结果:
{
"id":4,
"method":"eth_submitWork",
"params":["0x6b3c9a00673eeacc", #nonce值
"0xe41d8d6a87364b010c92c9a4ef44965ee801faf64b90dc9b40f71ff74eea3e02" #headerhash,
"0x2019a97ed06d889e93828c32fd065cf4c82de178462ae91ae9a93390e67730a4" #mix_hash满足条件的计算结果
],
"jsonrpc":"2.0"
}
client提交的ID是逐一递增的
nonce值:这是通过DAG图中随机抽取的一段值计算得得出的
headerhash:和任务下发的headerhash对应,链上会通过这个值匹配到下发的任务
结果:nonce值是在DAG图中找到的一段数字;
mix_hash就是用nonce和hearderhash经过设定的计算流程后,得到的满足难度要求的结果
方法:server是否接受:
如果任务匹配并通过,Server回复:
{
"id":4,
"jsonrpc":"2.0",
"result":true
}
如果Server不接受结果:
{
"id": 123,
"result": false,
"error": [
-1,
"Job not found",
NULL
]
}
?Ethash计算原理:
首先我们关注Ethhash计算的输入参数,和计算结果;参考前面的stratum协议
输入参数:headerhash和seedhash
计算结果:nonce值和mix_hash
计算过程:
先根据输入seedhash创建dag图:
bool static ethash_compute_cache_nodes(
node* const nodes, //存放DAG图的缓存
uint64_t cache_size, //DAG图缓存大小
ethash_h256_t const* seed //seedhash
)
{
//先确认cache_size大小无误
if (cache_size % sizeof(node) != 0) {
return false;
}
//计算该cache_size包含多少个nodes
uint32_t const num_nodes = (uint32_t) (cache_size / sizeof(node));
//先把seedhash经过sha3_512存入nodes[0].bytes
SHA3_512(nodes[0].bytes, (uint8_t*)seed, 32);
//循环对nodes[n].bytes 做SHA3_512
for (uint32_t i = 1; i != num_nodes; ++i) {
SHA3_512(nodes[i].bytes, nodes[i - 1].bytes, 64);
}
//循环交叉计算hash
for (uint32_t j = 0; j != ETHASH_CACHE_ROUNDS; j++) {
for (uint32_t i = 0; i != num_nodes; i++) {
uint32_t const idx = nodes[i].words[0] % num_nodes;
node data;
data = nodes[(num_nodes - 1 + i) % num_nodes];
for (uint32_t w = 0; w != NODE_WORDS; ++w) {
data.words[w] ^= nodes[idx].words[w];
}
SHA3_512(nodes[i].bytes, data.bytes, sizeof(data));
}
}
//做大小端转换
fix_endian_arr32(nodes->words, num_nodes * NODE_WORDS);
return true;
}
DAG图创建好后,就可以进行mix hash计算,计算流程如下图:
从图中可以看出计算输入值是headerhash和nonce值,nonce值是随机取的,headerhash和nonce值算出一个索引到DAG图中取出索引对应的数据后反复计算hash,获得一个结果;
但是这个结果不一定是符合要求的,所以会对结果做一个比较:
bool EthashCUDAMiner::report(uint64_t _nonce)
{
Nonce n = (Nonce)(u64)_nonce;
Result r = EthashAux::eval(work().seedHash, work().headerHash, n);
//r.value
if (r.value < work().boundary)
return submitProof(Solution{ n, r.mixHash });
return false;
}
如果符合难度要求就会向矿池上报计算结果,否则继续寻找符合难度要求的nonce值。
boundary越小,要找到符合的nonce值就需要经历越多的循环,难度就越高。
结合代码:
知道上述原理,我们在结合ethminer源码进行分析:
代码入口:
main
? ? ? ? ->cli.execute();
? ? ? ? ? ? ? ? ->doMiner();
doMiner中最主要的类是PoolManager
void doMiner()
{
new PoolManager(m_PoolSettings);
if (m_mode != OperationMode::Simulation)
for (auto conn : m_PoolSettings.connections)
cnote << "Configured pool " << conn->Host() + ":" + to_string(conn->Port());
// Start PoolManager 包括启动和运行,PoolManager中把stratum协议处理和miner计算关联起来
PoolManager::p().start();
// 信息输出可以先忽略
m_cliDisplayTimer.expires_from_now(boost::posix_time::seconds(m_cliDisplayInterval));
m_cliDisplayTimer.async_wait(m_io_strand.wrap(boost::bind(
&MinerCLI::cliDisplayInterval_elapsed, this, boost::asio::placeholders::error)));
// 正常该进程休眠,如果收到外部信号,执行退出
unique_lock<mutex> clilock(m_climtx);
while (g_running)
g_shouldstop.wait(clilock);
// 退出操作
if (PoolManager::p().isRunning())
PoolManager::p().stop();
cnote << "Terminated!";
return;
}
PoolManager::start()中调用了rotateConnect
void PoolManager::start()
{
m_running.store(true, std::memory_order_relaxed);
m_async_pending.store(true, std::memory_order_relaxed);
m_connectionSwitches.fetch_add(1, std::memory_order_relaxed);
//这里调用了rotateConnect
g_io_service.post(m_io_strand.wrap(boost::bind(&PoolManager::rotateConnect, this)));
}
通讯模块EthStratumClient:
rotateConnect中做了几件重要的事情:
void PoolManager::rotateConnect()
{
//创建Stratum处理类
p_client = std::unique_ptr<PoolClient>(
new EthStratumClient(m_Settings.noWorkTimeout, m_Settings.noResponseTimeout));
//setClientHandlers给p_client初始化一系列的回调函数,也就是stratum解析到哪些阶段就调用miner执行计算
if (p_client)
setClientHandlers();
//连接服务器开始工作
p_client->connect();
}
setClientHandlers 中设置了几个重要的回调函数
void PoolManager::setClientHandlers()
{
// 跟服务器建立起连接时调用
p_client->onConnected([&]() {
// Farm start会创建启动各计算模块(CUDAMINER,CPUMINER)
Farm::f().start();
})
// 跟服务断开连接时调用
p_client->onDisconnected([&]() {
}
p_client->onWorkReceived([&](WorkPackage const& wp) {
//stratum(p_client)通讯获取的任务信息交个计算模块
Farm::f().setWork(m_currentWp);
});
p_client->onSolutionAccepted([&]() {
//计算模块算出结果被服务器成功接受时,通知计算模块进行一些统计工作
Farm::f().accountSolution(_minerIdx, SolutionAccountingEnum::Accepted);
}}
}
初始化和connect过程完成后,stratum模块接收到的报文通过以下流程最终在processResponse里处理
connect_handler
? ? ? ? ->recvSocketData
? ? ? ? ? ? ? ? ->onRecvSocketDataCompleted
? ? ? ? ? ? ? ? ? ? ? ? ->processResponse
void EthStratumClient::onRecvSocketDataCompleted(
const boost::system::error_code& ec, std::size_t bytes_transferred)
{
if (jRdr.parse(line, jMsg))
{
try
{
// 处理接收到的消息,如果收到新任务将m_newjobprocessed设置为true
processResponse(jMsg);
}
catch (const std::exception& _ex)
{
cwarn << "Stratum got invalid Json message : " << _ex.what();
}
}
//如果有新任务,调用onWorkReceived处理,这个回调就是上文提到在setClientHandlers中设置的
//到这里通讯和计算模块的工作就串起来了
//上面processResponse中设置m_newjobprocessed为true,到这里就处理新任务
if (m_newjobprocessed)
if (m_onWorkReceived)
m_onWorkReceived(m_current);
}
计算模块(各miner):?
我们前文讲过通讯模块和计算模块关联的函数在onWorkReceived,调用了Farm::f().setWork(m_currentWp);函数让计算模块工作
Farm::f().setWork的工作如下:
void Farm::setWork(WorkPackage const& _newWp)
{
for (unsigned int i = 0; i < m_miners.size(); i++)
{
m_currentWp.startNonce = _startNonce + ((uint64_t)i << m_nonce_segment_with);
// 调用Farm管理的各miner模块,setWork
m_miners.at(i)->setWork(m_currentWp);
}
}
Miner是一个抽象类,Miner::setWork就做了一件事,把work赋值给了成员变量:
void Miner::setWork(WorkPackage const& _work){
..............
// 赋值给成员变量等待使用
m_work = _work;
}
继承Miner的类,通过调用Miner::work(),取得m_work
WorkPackage Miner::work() const
{
boost::mutex::scoped_lock l(x_work);
return m_work;
}
有很多Miner的实现继承了miner类,如CUDAMiner CPUMiner等类;CUDAMiner是调用NvidiaGpu加速计算过程的类,是经典实现,我们以该类为例继续分析
CUDAMiner中最重要的就是workLoop,在反复计算:
void CUDAMiner::workLoop()
{
while (!shouldStop())
{
if (current.epoch != w.epoch)
{
// 前文说过3000个块差不多100小时1个世纪,每次世纪更迭,要调用initEpoch
// 在initEpoch中会最终调用到函数ethash_generate_dag 生成dag图
if (!initEpoch())
break; // This will simply exit the thread
// As DAG generation takes a while we need to
// ensure we're on latest job, not on the one
// which triggered the epoch change
current = w;
continue;
}
// 寻找符合难度的nonce
search(current.header.data(), upper64OfBoundary, current.startNonce, w);
}
}
?看看search中做的事情:
void CUDAMiner::search(
uint8_t const* header, uint64_t target, uint64_t start_nonce, const dev::eth::WorkPackage& w){
while (!done)
{
volatile Search_results& buffer(*m_search_buf[current_index]);
// 如果计算过程有找到符合的结果,m_search_buf会被填充赋值
uint32_t found_count = std::min((unsigned)buffer.count, MAX_SEARCH_RESULTS);
// run_ethash_search调用GPU进行计算
if (!done)
run_ethash_search(
m_settings.gridSize, m_settings.blockSize, stream, &buffer, start_nonce);
if (found_count)
{
uint64_t nonce_base = start_nonce - m_streams_batch_size;
for (uint32_t i = 0; i < found_count; i++)
{
uint64_t nonce = nonce_base + gids[i];
// 计算结果上报服务器,和我们在stratum协议中说讲的输出值对应,nonce,mixes结果,w中有header函数
Farm::f().submitProof(
Solution{nonce, mixes[i], w, std::chrono::steady_clock::now(), m_index});
}
}
}
}
ethash_search中做的事情:
__global__ void ethash_search(volatile Search_results* g_output, uint64_t start_nonce)
{
uint32_t const gid = blockIdx.x * blockDim.x + threadIdx.x;
uint2 mix[4];
// 用start_nonce没找到符合的结果,退出,更换nonce继续计算
if (compute_hash(start_nonce + gid, mix))
return;
// 如果找到符合结果的值
uint32_t index = atomicInc((uint32_t*)&g_output->count, 0xffffffff);
if (index >= MAX_SEARCH_RESULTS)
return;
// 填充output buffer中的值,让外层函数可以获取结果
g_output->result[index].gid = gid;
g_output->result[index].mix[0] = mix[0].x;
g_output->result[index].mix[1] = mix[0].y;
g_output->result[index].mix[2] = mix[1].x;
g_output->result[index].mix[3] = mix[1].y;
g_output->result[index].mix[4] = mix[2].x;
g_output->result[index].mix[5] = mix[2].y;
g_output->result[index].mix[6] = mix[3].x;
g_output->result[index].mix[7] = mix[3].y;
}
compute_hash 调用GPU计算,过程复杂,这里就提结果判断这点
DEV_INLINE bool compute_hash(uint64_t nonce, uint2* mix_hash)
{
// 如果计算结果大于目标值,代表这次计算失败,返回true,进入下一轮计算
if (cuda_swab64(keccak_f1600_final(state)) > d_target)
return true;
// 如果计算结果小于目标值,代表这次计算结果有笑,填充到带出参数中,并返回
mix_hash[0] = state[8];
mix_hash[1] = state[9];
mix_hash[2] = state[10];
mix_hash[3] = state[11];
return false;
}
到此代码的主要过程就分析完了,涉及的算法的部分是最难的部分,也是精髓;希望有兴趣的朋友可以钻研分享;
CUDA编程:
说道Ethminer,很难不提起GPU编程;详细的部分朋友们可以在网上找相关资料;我研究不是很深,简单说下自己的理解;
GPU设计就是用很多小硬件核心,完成并行计算;既然是并行计算,要满足
1、计算过程,可以把数据分拆成解耦的小块,每个核完成一小块计算,最后拼成结果;核排列是二维的,所以最后的数据处理很像线性代数中的矩阵处理
2、GPU计算很快,但是内存跟不上上GPU计算速度;所以要把数据提前放入显存;并且一组GPU核可以高速同时访问同一块内存;所以在编程上出现每个GPU挪动一小块数据到内存中,一组GPU同时完成一组数据的迁移,这样可以加快内存访问速度
基于以上两点考虑,就让GPU编程的复杂度提升太多,很多CPU上可以理解的过程到GPU上就一头懵
ethminer的GPU算法我也没完全看明白,希望看懂得高手分享。但是dag图生成的代码相对简单,对照CPU的dag图代码还是可以看出一些端倪
?请擅长CUDA编程的朋友多指导
|