消息处理下篇-协义分析二
终于来到了最重点的部分咯~接续上篇和中篇,我们这次来讲解下载区块,区块头,以及交易。有什么用呢?下载完之后,比特币的所有的区块交易在本地就有了副本了,成为完整的比特币节点,完全跟比特币网络同步咯。随后,你想挖矿,建立区块浏览器都不是难事儿啦。
11、IBD 时,区块头部优先节点获取数据的处理
IBD 即初始区块下载的缩写。
在 IBD 期间,新的对等节点需要下载大量的区块来使本节点的区块链达到最佳高度。当节点设置了优先下载区块头部时,就会优先下载头部,然后再下载区块,而不是首先下载区块。现在新的比特币核心客户端基本上都是设置为优先下载区块头部,我们在本部分讲述这部分内容。
11.1、getheaders 消息
getheaders 消息作为服务器,响应客户端节点发送的请求多个头部消息。
代码在 net_processing.cpp 文件中的 ProcessMessage 方法的 2123 行。
以 if (strCommand == NetMsgType::GETHEADERS) { 为开始,具体如下:
从输入流中取得区块定位器和结束区块的哈希值。
CBlockLocator locator; uint256 hashStop; vRecv >> locator >> hashStop;如果区块定位器的
vHave数量大于规定的最大数量,那么断开节点的连接,并返回真。if (locator.vHave.size() > MAX_LOCATOR_SZ) { LogPrint(BCLog::NET, "getheaders locator size %lld > %d, disconnect peer=%d\n", locator.vHave.size(), MAX_LOCATOR_SZ, pfrom->GetId()); pfrom->fDisconnect = true; return true; }如果当前正处于 IBD 中,且远程节点又不在白名单中,则直接返回。 if (IsInitialBlockDownload() && !pfrom->fWhitelisted) { LogPrint(BCLog::NET, "Ignoring getheaders from peer=%d because node is in initial block download\n", pfrom->GetId()); return true; }
调用定位器的
IsNull方法,判断vHave属性集合是否为空。如果区块定位器
vHave集合为空,则:调用
LookupBlockIndex方法,返回停止位置的区块。如果停止位置的区块不存在,则直接返回。pindex = LookupBlockIndex(hashStop); if (!pindex) { return true; }方法内部从
mapBlockIndex集合里查找哈希对应的区块索引对象。调用
BlockRequestAllowed方法,判断是否允许当前请求。如果不允许,则直接返回。这里的判断是为了防止指纹攻击。
否则,即区块定位器
vHave集合不空,则:调用
FindForkInGlobalIndex方法,返回请求的区块索引。如果主链上最后一个区块的哈希存在,则调用活跃区块链的Next方法,请求下一个区块。FindForkInGlobalIndex方法内部遍历区块定位器的vHave集合,根据当前的哈希的,调用LookupBlockIndex方法,寻找其对应的区块索引对象,如果该索引对象存在且区块链中包含这个索引,则返回这个区块索引,否则如果该索引对象存在且其祖先是栈顶区块对象,那么返回栈顶元素的区块索引对象。如果找不到对应的索引对象,那么返回创世区块的索引对象。代码如下:
for (const uint256& hash : locator.vHave) { CBlockIndex* pindex = LookupBlockIndex(hash); if (pindex) { if (chain.Contains(pindex)) return pindex; if (pindex->GetAncestor(chain.Height()) == chain.Tip()) { return chain.Tip(); } } } return chain.Genesis();for 循环遍历活跃区块链上当前区块索引之后的区块,处理如下:
获取当前区块索引对象的区块头部,保存在
vHeaders变量中。如果已经达到最大长度限制 2000,或达到指定的停止位置,退出循环。
代码如下:
std::vector<CBlock> vHeaders; int nLimit = MAX_HEADERS_RESULTS; for (; pindex; pindex = chainActive.Next(pindex)) { vHeaders.push_back(pindex->GetBlockHeader()); if (--nLimit <= 0 || pindex->GetBlockHash() == hashStop) break; }设置节点状态对象的发送给对等节点的最佳区块索引为当前的区块索引对象或区块链的栈顶索引。
nodestate->pindexBestHeaderSent = pindex ? pindex : chainActive.Tip();调用
PushMessage消息,发送headers消息。connman->PushMessage(pfrom, msgMaker.Make(NetMsgType::HEADERS, vHeaders));
11.2 headers 消息
headers 消息作为客户端,响应服务器节点发送的多个头部消息。
代码在 net_processing.cpp 文件中的 ProcessMessage 方法的 2673 行。
以 if (strCommand == NetMsgType::HEADERS && !fImporting && !fReindex) 为开始标志,具体如下:
调用
ReadCompactSize方法,计算输入流传递的头部数量。如果头部数量大量 2000,则调用Misbehaving方法,处罚远程对等节点,然后返回。从输入流中取出所有的头部消息,保存到
headers变量中。调用
ProcessHeadersMessage方法,处理接收到的头部。下面我们具体看下这个方法的处理。
如果要请求的头部数量为空,则直接退出方法。
如果服务器节点发送的第一个头部对象对应用的区块不存在于
mapBlockIndex集合中,且请求头部的数量(即前面服务器节点返回的头部数量)小于 (MAX_BLOCKS_TO_ANNOUNCE)8,则进行如下处理:节点状态对象的未连接的头部数量增加1,即未能连到区块链上面的头部。
调用
PushMessage方法,发送getheaders消息,请求区块头部。调用
UpdateBlockAvailability方法,设置区块状态对象的相关属性。如果节点发送的未连接到区块链上的头部数量达到规定的最大数量 10,则调用 调用
Misbehaving方法,处罚远程对等节点。然后返回。
以上逻辑的代码如下:
遍历所有的收到的头部,对每个头部进行检查。
调用
ProcessNewBlockHeaders方法,检查每个收到的区块头部。ProcessNewBlockHeaders方法内部遍历每个收到的区块头部,调用validation.cpp文件中的CChainState对象的AcceptBlockHeader方法,检查是否可以接受当前的区块头部,如果不能接受则返回假。如果所有的区块头部都可以接受,则返回真。如果不能接受所有的头部 ,并且区块状态对象也不是 OK 的,那么进行不同的处理:
如果变量
nDoS大于0,调用Misbehaving方法,对远程节点进行惩罚,可能导致其被禁止发送。如果参数
punish_duplicate_invalid为真,且第一个无效的头部对应的区块索引存在,那么设置其断开连接。
以上逻辑的代码如下:
设置区块状态对象的未连接头部数量为 0。
CNodeState *nodestate = State(pfrom->GetId()); nodestate->nUnconnectingHeaders = 0;
调用
UpdateBlockAvailability方法,更新区块状态对象的相关属性。代码比较简单如下:如果收到的区块头部数量达到规定的最大值 2000 (
MAX_HEADERS_RESULTS),意味着对等节点还有更多的头部可以发送,那么调用PushMessage方法,发送getheaders消息,请求更多的区块头部。调用
CanDirectFetch方法,检测是否可以直接获取区块数据。方法内部比较区块链顶部区块的时间,代码如下:
如果可以获取数据,并且区块头部是有效的,并且当前区块链对象的工作量小于最后一个区块索引对象的工作量,进行下面的处理。
通过最后一个区块对象的
pprev指针,前向遍历所有的区块索引对象,把符合要求的区块对象加入vToFetch向量中。如果当前活跃区块链对象不包含
pindexWalk,那么反转vToFetch集合的顺序,然后进行遍历,根据每一个区块索引对象构造出一个CInv对象,并加入vGetData向量中。然后检查vGetData向量是否为空,如果不为空,则调用PushMessage方法,发送getdata消息,请求数据。如果当前处于
IBD中,且收到的区块头部数量未达到规定的最大值 2000 (MAX_HEADERS_RESULTS),即远程节点没有更多的头部可以发送,则进行下面的处理:最后,进行其他一些简单处理。
返回真。
11.3、getdata 消息
getdata 消息作为服务器,响应客户端节点发送的请求数据的消息。
代码在 net_processing.cpp 文件中的 ProcessMessage 方法的 1985 行。
以 if (strCommand == NetMsgType::GETDATA) { 为开始标志,具体如下:
从输入流中取得需要的数据,保存到
vInv变量中。如果发送的数量超过 50000 (
MAX_INV_SZ),则调用Misbehaving方法,对远程节点进行惩罚,可能导致其被禁止发送。保存请求数据的内容到节点的
vRecvGetData队列中。调用
ProcessGetData方法,处理客户端请求的数据。遍历
vRecvGetData队列,如果没有到队列尾部,且当前数据的类型是交易、或隔离见证类型的交易,则进行下面的处理:如果当前节点暂停发送,即
fPauseSend属性为真,则退出循环。获取当前的
CInv对象,如果交易映射mapRelay集合中有当前的交易,则调用PushMessage方法,发送tx消息;否则,如果节点请求过mempool,即timeLastMempoolReq属性不空,则从交易池中取得对应的交易。如果交易池中有指定的交易,且进入交易池中的时间在请求mempool的时间之后,那么调用PushMessage方法,发送tx消息。如果没有找到对应的交易,则把当前未到的 inv 对象放入未找到集合
vNotFound中。代码如下:
如果没有到队列尾部,且没有暂停发送,更进一步,当前请求数据的类型是区块、过滤区块、紧凑区块、隔离见证区块,则调用
ProcessGetBlockData方法,进行处理。ProcessGetBlockData方法的主体是调用PushMessage方法,发送区块、过滤区块、紧凑区块、隔离见证区块。清除
vRecvGetData队列从开始到当前位置之间所有已经处理过的元素。如果请求的某些数据没有找到,则发送
notfound消息。
11.3.1、ProcessGetBlockData
本方法的主要作用是处理客户端请求区块数据。实现如下:
初始化相关变量。
查找对应的区块对象。如果存在,且其保存的总的交易数量大于0,但是没有经过验证,那么设置变量
need_activate_chain为真。如果变量
need_activate_chain为真,则调用ActivateBestChain方法,激活区块链。如果区块索引对象存在,则调用
BlockRequestAllowed方法,确实是否允许发送区块对象给这个节点。如果允许发送,但是已经达到输出限制,则断开连接,并设置不允许发送。
如果允许发送,但节点不在白名单中,为了避免因为修剪而导致的泄漏,则断开连接,并设置不允许发送。
如果允许发送,并且区块数据存在于硬盘中,则进行下面的处理:
生成区块对象,并根据不同情况进行相应处理。
如果区块对象存在,则进行不同的处理。
如果请求的库存对象类型为区块,则调用
PushMessage方法,发送不带隔离见证信息的block消息。如果请求的库存对象类型为隔离见证型区块(详见 BIP144),则调用
PushMessage方法,发送带隔离见证信息的block消息。如果请求的库存对象类型为过滤型区块(详见 BIP37),并且设置了过滤器,则调用
PushMessage方法,发送merkleblock消息,然后发送所有的交易tx消息。如果请求的库存对象类型为紧凑型区块,则调用
PushMessage方法,发送cmpctblock消息或者block消息。
如果请求的库存对象的哈希等于节点的继续发送哈希,则调用
PushMessage方法,发送inv消息,继续请求节点发送区块对象。
11.4 客户端节点对服务器 getdata 消息回复的后续处理
getdata 消息回复的后续处理在 getdata 消息中,服务器根据请求数据的类型,可以发送区块消息、交易消息等。下面我们对这些消息分别进行说明。
11.4.1、block 消息
block 消息作为客户端,响应服务器节点发送的单个区块的消息。
代码在 net_processing.cpp 文件中的 ProcessMessage 方法的 2698 行。
以 if (strCommand == NetMsgType::BLOCK && !fImporting && !fReindex) 为开始标志,具体如下:
从输入流中取得发送的区块对象。
调用
ProcessNewBlock方法,处理新收到的区块。方法内部处理如下:
首先,调用
CheckBlock方法,检查区块是 OK 的。其次,如果第第 1 步检查是 OK 的,则调用区块链状态对象的
AcceptBlock方法,保存区块到硬盘上。然后,调用
NotifyHeaderTip方法,通知区块链栈顶已改变。最后,调用区块链状态对象的
ActivateBestChain方法,把区块连接到区块链上。方法主体是一个 while 循环。最主要的是调用
ActivateBestChainStep方法,激活最佳区块链,把新区块连接到主链上。ActivateBestChainStep方法内部调用ConnectTip方法来把区块链接到主链,内部调用ConnectBlock方法进行真正的连接。在把区块连接到区块链上之后,调用UpdateTip方法,更新栈顶区块。
如果是新的区块,则设置节点对象的
nLastBlockTime属性,否则,从mapBlockSource集合中移除对应的对象。返回真。
11.4.1.1、CheckBlock
本方法主要作用是检查区块是不是 OK 的。方法的第4、5 个参数 fCheckPOW、fCheckMerkleRoot 默认都为真。实现如下:
如果区块已经验证过,直接返回真。
调用
CheckBlockHeader方法,检查区块头部是否有效,特别是 POW。如果无效,返回假。CheckBlockHeader方法内部实现如下:如果需要检查默克尔头部,则:
首先,调用
BlockMerkleRoot方法,生成默克尔根。然后,比较区块对象的默克尔根与生成的根是否相同。如果不相同,则调用验证状态对象的
DoS方法,设置其相关属性,特别的如果mode不等于MODE_ERROR,则设置为MODE_INVALID。最后,如果变量
mutated为真,则调用验证状态对象的DoS方法,设置其相关属性。
以上逻辑的代码如下:
检查区块对象的交易数大小是否在指定范围内。
检查第一个交易是 coinbase 交易,其他交易不是。
检查每一个交易都是有效的。
检查签名的大小
如果参数
fCheckPOW、fCheckMerkleRoot都为真,则设置区块对象已经检查完成。返回真。
11.4.1.2、AcceptBlock
这个方法主要作用是把区块对象保存到硬盘上。方法的 ppindex、dbp 两个参数为空。实现如下:
初始化各个变量。
因为
ppindex为空,所以pindex默认为空指针。调用
AcceptBlockHeader方法,检查区块头部。AcceptBlockHeader方法执行如下:获取区块对象的哈希值,并初始其他变量
检查区块对象的哈希值是否不等于创世区块的哈希值。如果不等于,则进行下面的检查。
如果哈希对应的区块头部已经存在于
mapBlockIndex集合中,则检查验证状态是否为BLOCK_FAILED_MASK。如果是,则调用Invalid方法,设置相关的状态属性。接下来,检查区块头部。代码如下:
CheckBlockHeader方法,主要检查 PoW。如果检查失败,则调用DoS方法,设置相关的状态属性。再接下来,处理前一个区块的相关检查。
如果
pindex为空,则调用AddToBlockIndex方法,生成一个新的区块索引对象并进行初始化,然后添加到mapBlockIndex集合中。接下来,调用
CheckBlockIndex方法,检查所有的区块索引对象。最后,返回真。
初始化表示区块是不是已经存在的变量。
通过比较区块索引对象的总工作量与活跃区块链顶部区块索引的总工作量,确定是否还有更多的工作要做。
检查区块索引的高度是不是远远高于活跃区块链顶部。
MIN_BLOCKS_TO_KEEP表示不能修剪的最小调试,当前是 288。如果已经拥有这个区块,则直接返回真。
如果没有请求这个区块,则处理如下:
调用
CheckBlock、ContextualCheckBlock两个方法进行检查。如果任何一个方法检查不通过,则返回错误。其中,
CheckBlock方法,前面已经调用过,这里又一次调用。ContextualCheckBlock方法进行上下文检查。如果不是在 IBD,且区块链头部与指定区块的父区块相同,则调用
GetMainSignals().NewPoWValidBlock方法,处理新加入的区块。调用
SaveBlockToDisk方法,把区块保存到硬盘上。调用ReceivedBlockTransactions方法,标记区及其数据为已经收到和检查。if (fNewBlock) *fNewBlock = true; try { CDiskBlockPos blockPos = SaveBlockToDisk(block, pindex->nHeight, chainparams, dbp); if (blockPos.IsNull()) { state.Error(strprintf("%s: Failed to find position to write new block to disk", func)); return false; } ReceivedBlockTransactions(block, pindex, blockPos, chainparams.GetConsensus()); } catch (const std::runtime_error& e) { return AbortNode(state, std::string("System error: ") + e.what()); }
ReceivedBlockTransactions方法,主要是修改区块索引对象的相关属性。调用
FlushStateToDisk方法,刷新状态到硬盘上。FlushStateToDisk(chainparams, state, FlushStateMode::NONE);
调用
CheckBlockIndex方法,进行一些检查。CheckBlockIndex(chainparams.GetConsensus());
返回真。
11.4.1.3、ContextualCheckBlock
本方法主要进行区块相关的上下文检查。实现如下:
计算区块的高度。
强制使用 BIP113 的 MTP。
检查所有交易是否已经完成。
强制执行 coinbase 以序列化区块高度开始。
进行验证隔离见证相关的验证
验证 weight,具体见 BIP 141。
返回真。
11.4.2、cmpctblock 消息
cmpctblock 消息作为客户端,响应服务器节点发送的单个区块的消息。
代码在 net_processing.cpp 文件中的 ProcessMessage 方法的 2377 行。
以 if (strCommand == NetMsgType::CMPCTBLOCK && !fImporting && !fReindex) 为开始标志,具体如下:
获取服务器发送的紧凑区块对象。
查找前一个区块的索引对象,如果不存在,并且当前没有在 IBD,那么调用
PushMessage方法,发送getheaders消息。查找区块自身的索引对象,如果不存在,则设置变量
received_new_header为真。调用
ProcessNewBlockHeaders方法,验证接收到的区块头部对象。如果区块头部对象验证失败,则进行下面的处理。ProcessNewBlockHeaders方法处理过程,前面已经讲解过,此处不讲解。构造一个输入流。
初始化以下变量。
设置
UpdateBlockAvailability方法,更新区块索引对象的相关属性。如果获取的是一个新的头部,并且区块索引对象上保存的工作量大于活跃区块链顶部索引对象的工作量,则更新状态对象的
m_last_block_announcement属性。初始化变量
blockInFlightIt、fAlreadyInFlight。如果索引对象对应的区块已经在硬盘文件中,则直接返回。
if (pindex->nStatus & BLOCK_HAVE_DATA) // Nothing to do here return true;
区块索引对象上保存的工作量小于活跃区块链顶部索引对象的工作量,或者索引对象的交易数量不为0,进一步索引对象对应的区块哈希存在于
mapBlocksInFlight集合中,即我们已经有这个区块,重新请求这个区块。如果我们没有请求过这个区块,并且不能直接获取数据,那么直接返回真。
如果网络启用了隔离见证,但是状态对象不支持隔离见证,直接返回真。
如果区块索引的高度小于活跃区块链高度加 2,则进行下面的处理:
如果本地没有请求过这个区块,并且状态对象的
nBlocksInFlight属性小于允许从同一个节点重新请求的区块数量,或者本地有请求过这个区块,并且blockInFlightIt对应的节点是当前节点,那么:调用
MarkBlockAsInFlight方法,把区块的哈希放在mapBlocksInFlight集合中。如果方法返回假,即区块的哈希已经存在于集合中,进行如下处理:调用
PartiallyDownloadedBlock对象的InitData方法,对紧凑区块进行处理。生成并初始化
BlockTransactionsRequest对象,向远程节点请求区块中包含的交易数据。
否则,区块或者已经在 flight ,或者对等节点有太多未完成的块可以下载,从而进行下面处理。
否则,即区块索引的高度不小于活跃区块链高度加 2,则进行下面的处理:
如果已经请求过这个区块,但可能与我们目前已有的数据相关太远,所以没用,要重新发起请求,否则,即这个区块是公布的,那么设置
fRevertToHeaderProcessing为真。如果变量
fProcessBLOCKTXN为真,即所有的区块交易都是可用的,那么调用ProcessMessage方法,处理这些交易。如果变量
fRevertToHeaderProcessing为真,那么调用ProcessHeadersMessage方法,处理头部消息。如果变量
fBlockReconstructed为真,进行下面的处理。返回真。
11.4.3、tx 消息
tx 消息作为客户端,响应服务器节点发送的交易消息。
代码在 net_processing.cpp 文件中的 ProcessMessage 方法的 2190 行。
以 if (strCommand == NetMsgType::TX) { 为开始标志,具体如下:
如果不允许中继交易,并且节点不在白名单中,则直接返回真。
从输入流中取得服务器节点发送的交易信息
构造
CInv对象,并调用节点的AddInventoryKnown方法,把库存对象加入filterInventoryKnown集合中。从集合的请求集合和
mapAlreadyAskedFor集合中移除已收到的库存的对象。调用
AlreadyHave确定是否拥有这个库存对象,调用AcceptToMemoryPool方法,确定是否可以把这个库存对象加入内存池。如果两都满足,则进行如下处理:调用
RelayTransaction方法,中继收到的交易。方法内部生成库存对象,通过调用所有节点的
PushInventory方法,放入每个节点的setInventoryTxToSend或者vInventoryBlockToSend集合。把交易的所有输出放入变量
vWorkQueue集合中。更新节点最后一次收到交易的时间。
生成变量
std::set<NodeId> setMisbehaving;。遍历所有的交易输出,进行下面的处理:
从
mapOrphanTransactionsByPrev集合中查找当前输出对应的孤儿交易集合。同时从输出集合中删除当前的输出。如果当前的输出没有对应的孤儿交易,则处理下一个。遍历当前输出对应的孤儿交易。
首先,取得当前孤儿交易对应的相关变量。
然后,判断
setMisbehaving集合中是否包含这个节点ID。如果包含,则处理下一个。接下来,如果可以把这个孤儿交易放入内存池中,则把这个孤儿交易对应的输出放入
vWorkQueue集合,同时把这个孤儿交易放入删除的孤儿集合vEraseQueue中。否则,如果不能把这个孤儿交易放入内存池中,并且变量
fMissingInputs2为假,则进行不同的处理。如果发送的是无效的孤儿交易,则惩罚节点;如果有输入,但不能放入内存池中,可能是非标准交易,则放入删除的孤儿集合vEraseQueue中。
遍历所有因为收到这个交易而需要从孤儿交易队列中删除的交易,调用
EraseOrphanTx方法,删除对应的交易。
否则,如果缺少输入,则进行如下处理:
检查所有交易的输入引用的输出是否被拒绝。
如果所有交易的输入引用的输出都没有被拒绝,那么根据这些输入生成对应的库存对象,并加入节点的
filterInventoryKnown集合中,同时,把当前的交易加入孤儿交易集合中。相反,如果有某个交易的输入引用的输出被拒绝,则把交易加入recentRejects集合中。
以上两者都不符合,则
根据交易是隔离见证数据 和 witness-stripped 交易,进行不同的处理。
if (!tx.HasWitness() && !state.CorruptionPossible()) { recentRejects->insert(tx.GetHash()); if (RecursiveDynamicUsage(ptx) < 100000) { AddToCompactExtraTransactions(ptx); } } else if (tx.HasWitness() && RecursiveDynamicUsage(ptx) < 100000) { AddToCompactExtraTransactions(ptx); }
如果节点在白名单中,且指定了白名单强制中继交易,进行下面的处理。
如果
lRemovedTxn集合不空,则加入vExtraTxnForCompact集合中。根据验证结果,进行不同的处理。
返回真。
11.4.4、inv 消息
inv 消息作为客户端,响应服务器节点发送的库存的消息。
代码在 net_processing.cpp 文件中的 ProcessMessage 方法的 1928 行。
以 if (strCommand == NetMsgType::INV) { 为开始标志,具体如下:
从输入流中取得库存消息
vInv。如果发送库存消息的数量大于inv消息允许的最大长度 50000,则:调用Misbehaving方法,对远程节点进行惩罚,可能导致被禁止;然后退出处理。判断是否只允许发送区块。如果是白名单节点,则可以发送区块。
遍历所有的库存消息,进行如下处理:
调用
AlreadyHave方法,判断是否已经有这个库存。根据是否为交易消息,对消息标志进行处理。
如果是区块消息,则进行如下处理:
调用
UpdateBlockAvailability方法,更新区块的状态信息。如果没有这个库存,且
fImporting为假,且不重建索引,且mapBlocksInFlight中不包括这个库存,则调用PushMessage方法,发送getheader消息。
否则,把库存消息加入到节点的
filterInventoryKnown集合。如果不是只接收区块数据,且fImporting为假,且 不是重建索引,且不在 IBD 中,则调用节点的AskFor方法,进行处理,这个方法的主体是把库存消息加入mapAskFor集合,除此之外,也进行一些其他处理,在些不详细说明。
返回真。
12、IBD 时,区块优先节点获取区块消息
在 IBD 期间,节点需要下载大量的区块来使本节点的区块链达到最佳高度。这又分为区块优先和头部优先两种情况,前面讲了头部优先下载,这是当前的主流方式。下面简要讲解下区块优先方式,这是老的客户端工作方式。
12.1、getblocks 消息
getblocks 消息作为服务器,响应客户端节点发送的请求区块消息。
代码在 net_processing.cpp 文件中的 ProcessMessage 方法的 2006 行。
以 if (strCommand == NetMsgType::GETBLOCKS) { 为标志,具体如下:
从输入流中取得区块定位器和结束区块的哈希值。
如果客户端请求的定位器
vHave数量超过规定的最大值,则设置客户端节点断开,并返回真。调用
ActivateBestChain方法,激活最佳的区块链。调用
FindForkInGlobalIndex方法,查找主链上客户端请求的最后一个区块的哈希值。这个方法的实现比较简单:
如果需要的最后一个区块存在,那么找到它的下一个区块。
if (pindex) pindex = chainActive.Next(pindex);
进行 for 循环,直到链的最顶端,或达到指定的结束区块。具体处理如下:
如果当前区块就是要停止的区块,则退出循环。
如果处于修剪模式下,并且当前区块索引不在硬盘上或者当前区块索引代码的区块的高度与区块链顶部区块的高度之差大于根据规定计算出来的值,则退出循环。
根据当前的区块索引生成库存对象,并保存到节点的
vInventoryBlockToSend集合中。如果达到当前最大的区块数量(当前500),则设置节点的继续位置为当前区块的哈希值。
以上逻辑的代码如下:
返回真。
注:
inv消息由后台线程定时发送。
13、紧凑区块中的交易消息处理
13.1、getblocktxn 消息
getblocktxn 消息代码在 net_processing.cpp 文件中的 ProcessMessage 方法的 2074 行。
以 if (strCommand == NetMsgType::GETBLOCKTXN) { 为标志,具体如下:
从输入流中取得请求对象。
如果请求的交易所在的区块就是最近的区块,则设置变量
recent_block。如果变量不空,则调用SendBlockTransactions方法,发送区块的交易,然后返回真。
调用
LookupBlockIndex方法,查找客户端请求的区块索引。如果不存在请求的索引,或者索引对应的区块数据不在硬盘上,则直接返回。如果请求区块的高度与区块链顶部的高度之差大于规定的最大深度 10 (
MAX_BLOCKTXN_DEPTH),则生成一个库存对象,并加入节点的vRecvGetData集合中,然后返回真。vRecvGetData集合中的数据由定时器定时处理。以上情况都不是,则从硬盘中加载区块,并进行发送。
返回真。
13.2、blocktxn 消息
blocktxn 消息作为客户端,处理服务器节点发送的区块中的交易。
代码在 net_processing.cpp 文件中的 ProcessMessage 方法的 2598 行。
以 if (strCommand == NetMsgType::BLOCKTXN && !fImporting && !fReindex) 为标志,具体如下:
从输入流中取得服务器发送的交易。
如果接收到的交易所在的区块不存在于
mapBlocksInFlight集合中,或者不是当前节点,则直接退出,并返回真。从部分下载的区块中填充区块对象。如果服务器发送的区块是无效的,那么调用
Misbehaving方法,惩罚远程节点;否则,如果不能处理区块数据,则生成一个库存对象,然后调用PushMessage消息,重新发送getdata消息;否则,即发送的区块是OK的,则调用MarkBlockAsReceived方法,设置节点状态对象的相关属性,同时把接收到的区块相关内容保存在mapBlockSource集合中。如果区块数据可以读取,那么调用
ProcessNewBlock方法,处理收到的区块。如果这个区块是新的,则修改节点的最后一次接收到区块的时间,否则从mapBlockSource集合中,移除区块对应的数据。返回真。
Last updated
Was this helpful?