最近在研究比特币源码, 发现很难有个突破点. 作为程序员, 在分析一个东西的时候, 很喜欢站在线程的角度来看这个东西. 于是就有了这篇文章. 作为以后深入分析源码的契机.
本文分析基于当前master(2018.7), 最近一个tag是v0.16.2rc1
基本线程模型
下图展示了新交易, 新区块和挖矿三个过程的线程交互模式.
其中Bitcoind process是一个不包含挖矿功能的全节点(新版本中已经移除了单独的挖矿线程), OtherNode是逻辑上与Bitcoind对等的其他节点, SpendProducer即交易产生节点, 大多数情况下它来自钱包或者其他节点的广播, SingleMiner可以理解为一个矿池中的矿工角色(单挖矿功能).
Bitcoind process包含了很多线程, 这儿只列出了比较核心的三个:
- HttpServer(接受并处理rpc请求, 是一组线程), 所有通过tableRPC注册的命令最终都是在此线程中执行
- ThreadSocketHandler(线程入口函数名), 处理节点间TCP连接的底层交互操作, socket读写等
-
ThreadMesageHandler(线程入口函数名), 主要处理由ThreadSocketHandler读写来的消息, 节点间交互逻辑的主要执行者
过程详细说明:
此处以新交易处理流程, 新区块处理流程和矿池中的矿工挖矿交互流程为例. 从这些交互的例子中也能有大概印象HttpServer和ThreadSocketHandler的交互侧重点差异所在.
新交易过程: (从New Transaction开始)
- 交易产生节点创建一笔交易后(交易数据结构,以下简称ntx, 是new transaction的简写), 通过inv消息将ntx发送该其连接的节点.
- 转换为全节点视角, 当全节点收到ntx后, 会将ntx异步发送给ThreadMesageHandler(共享内存方式, 线程间通信).
- ThreadMesageHandler处理对应消息(ProcessMessage函数的NetMsgType::TX对应逻辑):
- checkWork, 主要在AcceptToMemoryPoolWorker函数中执行, 检查包括合规性检查, 最小交易费, 双花检查, 执行解锁脚本等等
- memPool update, 基于上一步的检查结果, 将新交易加入memPool. memPool中包含了很多集合, 此步骤具体细节将在以后的文章中说明.
- RELAY, 将ntx广播出去. 具体做法是将一个inv消息追加到每个node的发送队列中, 等ThreadMesageHandler线程自动检测到此消息并发送给对应节点.
- 其他节点收到ntx后, 如果是全节点, 将会重复此新交易过程处理步骤.
新区块到达:(从New Block消息开始)
- 当一个矿池挖出一个新区块后, 这个矿池会尽快将此区块广播出去.
- 转换为收到新区块的全节点视角, 当全节点收到一个新区块后, 会消息异步发送给ThreadMesageHandler(同新交易过程).
- ThreadMesageHandler处理对应消息(ProcessMessage函数的NetMsgType::CMPCTBLOCK对应逻辑):
- CheckHeader 主要检查新区块与当前最长链的关系, 此处假设新区块的previous block就是当前的最长链的最后一个区块, 假设其他所有检查都通过.
- 进入ProcessNewBlock函数, 进行区块检查, 包括MerkleRoot, 所有交易合法性, SigOps等等.
- 主链接受新区块
- 更新memPool, 主要是将新区块中包含的交易信息从memPool中移除.
- RELAY, 将新区块广播出去, 具体做法就是发送一个NewPoWValidBlock, 其连接的处理函数会将一个CMPCTBLOCK类型的消息写入所有连接Node的发送队列中, 等ThreadMesageHandler线程自动检测到此消息并发送给对应节点.
- 其他节点收到新区块后, 如果是全节点, 将重复此新区块到达处理步骤.
矿工挖矿流程:(从getblocktemplate开始)
新版本去掉了内置的挖矿线程, 挖矿程序默认是一个单独进程, 通过此接口获得包括最新交易在内的区块模板, 并在此模板基础上挖矿.
- 矿工向全节点请求一个最新区块模板
- 全节点从其memPool中选出有价值的交易,产生区块头及CoinBase交易组装成区块模板
- 全节点将区块模板及其他限制参数(范围,大小等)写入返回值中, 返回给矿工
- 矿工开始挖矿
- 矿工挖矿成功后, 将新区块回传给全节点
- 全节点收到新区块后, 对其进行简单检查(区块hash, previousHash等), 然后进行新区块处理流程. 具体流程与新区块到达一致, 此不赘述
- RELAY, 中心节点将新区块广播出去, 与新区块处理流程一致, 此不赘述
- 收到新区块的其他节点, 如果是全节点, 将重复新区块到达处理步骤
挖矿核心代码
在前文一再说明, 新版本已经将挖矿的线程移除, 挖矿程序一般是一个独立运行的进程, 但在新分支中依然能够看到挖矿过程的代码. 此处去掉了与挖矿无关的操作, 只留下了核心代码以方便理解. 通过这些代码可以让我们对挖矿过程有个感性上的认识.
bool mine(CBLock* pblock, long nMaxTries, int nInnerLoopCount){
while(true){
updateExternNonce(pblock); // modify part of baseCoin tx and, at the same time, merkleRoot has been changed
while(blockTemplate->nonce == nInnerLoopCount || nMaxTries == 0 || !CheckProofOfWork(pblock->GetHash(), pblock->nBits)){
++blockTemplate->nonce ;
nMaxTries --;
}
if(blockTemplate->nonce == nInnerLoopCount ){
blockTemplate->nonce=0;
continue;
}
if(nMaxTries == 0) break;
return true;
}
return false;
}
从代码中我们看到, 调用挖矿方法时, 调用者通过设置最大重试次数的方式控制挖矿运行时长. 每次挖矿事实上是通过调整nonce或者extraNonce的方式改变区块hash值, 进而完成一次挖矿尝试, 当找到小于难度值的hash值时代表挖矿成功.
nonce是区块头中专门为调整区块hash头而预留的可变字段.
extraNonce是区块第一笔交易(CoinBase交易)的解锁脚本的一部分. 因为CoinBase交易不需要解锁脚本, 所以这个区域可以被用作其他一些用途. CoinBase的解锁脚本被更改后, 其hash值会产生变化, 进而使整个区块的merkleRoot值发生变化, 进而导致区块头发生变化. 区块头发生变化后, 挖矿就又可以从0开始尝试nonce字段了.
BTech原创,未经许可不得转载