比特币源码解读-消息处理

  ----红亚大学链:yjh、bjgpdn

  在比特币网络中,节点之间需要经常的进行消息交换,以保证区块链同步,比如向对方发送版本version消息,查看顶端区块hash的getblock消息以及传播区块的block消息。在比特币源码解读-P2P网络一文中,我们已经说明了节点之间是如何连接,接收和发送消息的。本文着重讲述这些消息的内容和意义及比特币0.01源码中如何处理收到的消息。

1. 信息交换的过程

1.1 消息的格式

  消息=消息头+数据,具体格式如下表:

  其中Message start字段是固定的,在源码中的定义是:

static const char pchMessageStart[4] = { 0xf9, 0xbe, 0xb4, 0xd9 };

1.2 消息的类型

1.3 消息交互过程

1.3.1 节点连接

  当建立一个或多个节点后新节点将一条包含自身IP地址的addr消息发送给其相邻接点。相邻接点再将这条addr消息转发给他们的相邻节点,保证新节点被更多的节点所接受。新加入的节点还可以像相邻节点发送getaddr节点消息来请求相邻接点已知的的对等节点IP


1.3.2 库存清单交换

  首先区块链分为全节点和非全节点,全节点是指存储了比特币完整区块数据库的节点,还有一些节点没有能力去存储这些节点,只能进行交易,只能做简易验证,且依赖全节点,这种叫做非全节点。

  ⼀个全节点连接到对等节点之后,第⼀件要做的事情就是构建完整的区块链。该节点首先向相邻节点发送version消息,该消息中包含BestHeight字段标示了自己的区块高度。通过互相发送version消息,对等节点就可得知双方的区块数量。

  对等节点还会发送getblocks消息,该消息中包含了本节点保存的区块链顶端的区块hash值,如果一个节点收到的hash在自己的区块链中不属于顶部,那么就代表自己的链比较长。

  拥有更长区块链的节点会识别出其他节点需要补充的块,从而发送库存inv(inventory)消息,这个消息会包含块的hash值,从而告知其他节点这些块的存在。收到库存消息的节点发现自己缺少块,就会向周围节点(未必是发送库存节点)发送getdata消息,来请求具体某些块的数据,从而补全自己。收到getdata消息的节点再把节点请求的块数据发送出去。

2. 消息处理源码解析

2.1 消息处理函数

  比特币源码专门启动一个线程来轮询每个节点,处理他们的消息。

  下面是比特币0.01源码中线程函数ThreadOpenConnections函数解读:

loop
{
 // 轮询链接的节点用于消息处理
 vector<CNode*> vNodesCopy;
 CRITICAL_BLOCK(cs_vNodes)
 vNodesCopy = vNodes;
 foreach(CNode* pnode, vNodesCopy)
 {
 pnode->AddRef();
 // Receive messages
 TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
 ProcessMessages(pnode);
 // Send messages
 TRY_CRITICAL_BLOCK(pnode->cs_vSend)
 SendMessages(pnode);
 pnode->Release();
 }
 ......
}

  这段程序的逻辑主体就是循环遍历vNodes节点链表,并对每个节点分别进行接收消息的处理和发送消息的处理。增加引用和释放引用的目的是防止在消息处理的过程中,节点被丢进节点断开连接池。下面分别对消息接收和消息发送进行讲解。

2.2 接收消息的处理

  下面是接收消息处理函数ProcessMessages内容的解析:

CDataStream& vRecv = pfrom->vRecv;
if (vRecv.empty())
 return true;

  首先取得节点的接收缓冲区中的信息到vRecv数据流中。

  以下内容为程序主体,位于loop循环中:

CDataStream::iterator pstart = search(vRecv.begin(),vRecv.end(),BEGIN(pchMessageStart),END(pchMessageStart));
// 删除无效的消息: 就是在对应的消息开始前面还有一些信息
if (vRecv.end() - pstart < sizeof(CMessageHeader))
{
 if (vRecv.size() > sizeof(CMessageHeader))
 {
 printf("\n\nPROCESSMESSAGE   MESSAGESTART NOT FOUND\n\n");
 vRecv.erase(vRecv.begin(), vRecv.end() - sizeof(CMessageHeader));
 }
 break;
}
if (pstart - vRecv.begin() > 0)
 printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin());
vRecv.erase(vRecv.begin(), pstart); // 移除消息开始信息和接收缓冲区开头之间

  使用search函数找到Message star开始的位置,删除消息头之前的无效信息。

CMessageHeader hdr;
vRecv >> hdr; // 指针已经偏移了
if (!hdr.IsValid())
{
 printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str());
 continue;
}
string strCommand = hdr.GetCommand();

  创建一个消息头对象hdr,将vRecv中的序列化数据反序列化到hdr中,然后判断消息头是否合理,主要是判断Messagestar字段是否正确,命令字段是否符合规范,消息大小是否过大。之后获得命令字符串strCommand。值得注意的是vRecv的>>调用会随着读取数据而偏移读取指针,这里在读取完头部信息后,读取指针就会指向数据域的开始。下次再开始时会直接读取数据域。

unsigned int nMessageSize = hdr.nMessageSize;
if (nMessageSize > vRecv.size())
{
 vRecv.insert(vRecv.begin(), BEGIN(hdr), END(hdr));
 Sleep(100);
 break;
}

  如果消息的真正大小小于nMessageSize,插入一个消息头,并退出循环

CDataStream vMsg(vRecv.begin(), vRecv.begin() + nMessageSize, vRecv.nType, vRecv.nVersion);
vRecv.ignore(nMessageSize);

  将消息复制到vMsg中,并将vRecv的读取指针向后偏移nMessageSize的长度(相当于读取完)。

static map<unsigned int, vector<unsigned char>   > mapReuseKey;
 // 消息采集频率进行处理
if (nDropMessagesTest >   0 && GetRand(nDropMessagesTest) ==   0)
{
 printf("dropmessages   DROPPING RECV MESSAGE\n");
 return true;
}

  这里有个消息采集频率的问题,消息不是每次都会被处理,而是有个概率值。

  以上是消息的预处理内容,主要针对消息的格式进行检查。

  下面是针对消息内容进行相应操作。

if (strCommand == "version")
{
 // 节点对应的版本只能更新一次,初始为0
 if (pfrom->nVersion != 0)
 return false;
 int64 nTime;
 CAddress addrMe; // 读取消息对应的内容
 vRecv >> pfrom->nVersion >> pfrom->nServices >> nTime >> addrMe;
 if (pfrom->nVersion == 0)
 return false;
 // 更新发送和接收缓冲区中的对应的版本
 pfrom->vSend.SetVersion(min(pfrom->nVersion, VERSION));
 pfrom->vRecv.SetVersion(min(pfrom->nVersion, VERSION));
 
 // 如果节点对应的服务类型是节点网络,则对应节点的客户端标记就是false
 pfrom->fClient = !(pfrom->nServices & NODE_NETWORK);
 if (pfrom->fClient)
 {
 // 如果不是节点网络,可能仅仅是一些节点不要保存对应的完整区块信息,仅仅需要区块的头部进行校验就可以了
 pfrom->vSend.nType |= SER_BLOCKHEADERONLY;
 pfrom->vRecv.nType |= SER_BLOCKHEADERONLY;
 }
 // 增加时间样本数据:没有什么用处,这函数好像压根没执行
 AddTimeData(pfrom->addr.ip, nTime);
 // 对第一个进来的节点请求block信息
 static bool fAskedForBlocks;
 if (!fAskedForBlocks && !pfrom->fClient)
 {
 fAskedForBlocks = true;
 pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0));
 }
}
 else if (pfrom->nVersion == 0)
 return false;

  如果命令字符串的内容是”version”,说明这是version消息。本地存储的其他节点的版本信息初试为0,且只能更新一次。所以如果该节点的版本已经不是0了,则version消息无效,直接返回。之后更新发送和接收缓区的版本信息。设置节点客户端标记,如果是网络节点,则代表这些节点不是完整节点,可能是建议客户端,则设置客户端标记fClient为true,然后将节点缓冲区类型设置为仅区块头。

  设置静态局部标记 fAskedForBlocks,这么写保证了只对第一个连接到的节点发送getblock消息,推送getblock消息到该节点缓冲区。这个消息后续会讲到。

  另外节点在处理任何version外消息之前版本号必须不为零。

else if (strCommand == "addr")
{
 vector<CAddress> vAddr;
 vRecv >> vAddr;
 
 // Store the new addresses
 CAddrDB addrdb;
 foreach(const CAddress& addr, vAddr)
 {
 if (fShutdown)
 return true;
 // 将地址增加到数据库中
 if (AddAddress(addrdb, addr))
 {
 // Put on lists to send to other   nodes
 pfrom->setAddrKnown.insert(addr); // 将对应的地址插入到已知地址集合中
 CRITICAL_BLOCK(cs_vNodes)
 foreach(CNode* pnode, vNodes)
 if (!pnode->setAddrKnown.count(addr))
 pnode->vAddrToSend.push_back(addr);// 地址的广播
 }
 }
}

  如果命令字符串的内容是”addr”,则说明这是地址分享的消息,节点可通过该消息发送一个地址列表给其他节点。首先将序列化的数据反序列化到vAddr中,遍历该地址数组。使用AddAddree函数将该地址加入到数据库。并将该地址插入到这个节点的setAddrKonwn。这之后遍历节点,把这个地址放入节点待发送地址列表,等待广播。

else if (strCommand == "inv")
{
 vector<CInv> vInv;
 vRecv >> vInv;
 
 CTxDB txdb("r");
 foreach(const CInv& inv, vInv)
 {
 if (fShutdown)
 return true;
 pfrom->AddInventoryKnown(inv); // 将对应的库存发送消息增加到库存发送已知中
 
 bool fAlreadyHave = AlreadyHave(txdb, inv);
 if (!fAlreadyHave)
 pfrom->AskFor(inv);// 如果不存在,则请求咨询,这里会在线程中发送getdata消息
 else if (inv.type == MSG_BLOCK && mapOrphanBlocks.count(inv.hash))
 pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), GetOrphanRoot(mapOrphanBlocks[inv.hash]));
 }
}

  如果命令字符串的内容是”inv”,则说明这个是发送库存的命令。拥有更长区块链的对等节点块的对等节点可以察觉出自己拥有更多区块,它会识别出第一批可供分享的500个区块并会把这些区块hash值传播出去。库存CInv类的hash成员保存的就是区块hash。输入本地节点发现这个库存是自己不知道,那么就会将这个库存存入到待请求列表。线程后续将会发送getblock消息来请求块具体信息。

else if (strCommand == "getdata")
{
 vector<CInv> vInv;
 vRecv >> vInv;
 
 foreach(const CInv& inv, vInv)
 {
 if (fShutdown)
 return true;
 printf("received getdata for:   %s\n", inv.ToString().c_str());
 
 if (inv.type == MSG_BLOCK)
 {
 // Send block from disk
 map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(inv.hash);
 if (mi != mapBlockIndex.end())
 {
 CBlock block;
 block.ReadFromDisk((*mi).second, !pfrom->fClient);
 pfrom->PushMessage("block", block);// 获取数据对应的类型是block,则发送对应的块信息
 }
 }
 else if (inv.IsKnownType())
 {
 // Send stream from relay memory
 CRITICAL_BLOCK(cs_mapRelay)
 {
  map<CInv, CDataStream>::iterator mi = mapRelay.find(inv); // 重新转播的内容
 if (mi != mapRelay.end())
 pfrom->PushMessage(inv.GetCommand(), (*mi).second);
 }
 }
 }
}

  如果命令字符串的内容是”getdata”,则说明这是请求数据消息。该消息可以向其他节点请求某些数据信息。

  将数据域信息反序列化到一个库存数组。遍历这个库存数组,如果库存的类型是MSG_BLOCK,代表请求的是块具体数据。节点查看本地是否有这个库存对应的块信息,如果有,将会推送block消息,线程后续会将块消息发送给请求节点。如果消息是其他已知类型,则发送转播表中对应该库存的内容。

else if (strCommand == "getblocks")
{
 CBlockLocator locator;
 uint256 hashStop;
 vRecv >> locator >> hashStop;
 //找到本地有的且在主链上的
 CBlockIndex* pindex = locator.GetBlockIndex();
 // 将匹配得到的块索引之后的所有在主链上的块发送出去
 if (pindex)
 pindex = pindex->pnext;
 printf("getblocks %d to %s\n", (pindex ? pindex->nHeight : -1), hashStop.ToString().substr(0,14).c_str());
 for (; pindex; pindex = pindex->pnext)
 {
 if (pindex->GetBlockHash() == hashStop)
 {
 printf(" getblocks stopping at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,14).c_str());
 break;
 }
 
 // Bypass setInventoryKnown in case   an inventory message got lost
 CRITICAL_BLOCK(pfrom->cs_inventory)
 {
 CInv inv(MSG_BLOCK, pindex->GetBlockHash());
 // 判断在已知库存2中是否存在
 if (pfrom->setInventoryKnown2.insert(inv).second)
 {
 pfrom->setInventoryKnown.erase(inv);
 pfrom->vInventoryToSend.push_back(inv);// 插入对应的库存发送集合中准备发送,在另一个线程中进行发送,发送的消息为inv
 }
 }
 }
}

  如果命令字符串的内容是”getblock”,是库存比较信息。对等节点之间互相发送该信息,当节点发现收到的getblock中的hash在自己的区块链中不是顶端,则说明自己的区块链比较长,于是向较短节点发送inv信息。

  首先将数据反序列到locator和hashStop对象中,分别对应初始块和结尾块。本节点将从loactor所对应区块索引还是向后遍历链表,并生成inv,等待发送。

else if (strCommand == "tx")
{
vector<uint256> vWorkQueue;
CDataStream vMsg(vRecv);
CTransaction tx;
vRecv >> tx;
 
CInv inv(MSG_TX, tx.GetHash());
pfrom->AddInventoryKnown(inv);// 将交易消息放入到对应的已知库存中
 
bool fMissingInputs = false;
// 如果交易能够被接受
if (tx.AcceptTransaction(true, &fMissingInputs))
{
 AddToWalletIfMine(tx, NULL);
 RelayMessage(inv, vMsg);// 转播消息
 mapAlreadyAskedFor.erase(inv);
 vWorkQueue.push_back(inv.hash);
 
 // 递归处理所有依赖这个交易对应的孤儿交易
 for (int i = 0; i < vWorkQueue.size(); i++)
 {
 uint256 hashPrev = vWorkQueue[i];
 for (multimap<uint256, CDataStream*>::iterator mi = mapOrphanTransactionsByPrev.lower_bound(hashPrev);
 mi != mapOrphanTransactionsByPrev.upper_bound(hashPrev);
 ++mi)
 {
 const CDataStream& vMsg = *((*mi).second);
 CTransaction tx;
 CDataStream(vMsg) >> tx;
 CInv inv(MSG_TX, tx.GetHash());
 
 if (tx.AcceptTransaction(true))
 {
 printf(" accepted orphan tx %s\n", inv.hash.ToString().substr(0, 6).c_str());
 AddToWalletIfMine(tx, NULL);
 RelayMessage(inv, vMsg);
 mapAlreadyAskedFor.erase(inv);
 vWorkQueue.push_back(inv.hash);
 }
 }
 }
 
 foreach(uint256 hash, vWorkQueue)
 EraseOrphanTx(hash);
}
else if (fMissingInputs)
{
 printf("storing orphan tx %s\n", inv.hash.ToString().substr(0, 6).c_str());
 AddOrphanTx(vMsg); // 如果交易当前不被接受则对应的孤儿交易
}
}

  如果命令字符串的内容是”tx”,则是交易信息。将交易信息反序列化到CTransaction类中。创建MSG_TX库存,把交易hash填入该库存。之后把库存放入已知库存表中。如果这笔交易信息符合规定(AcceptTransaction,主要是判断交易内容合不合适是不是已花费什么的),之后如果当前交易属于本节点,则将当前交易加入到钱包中。这之后后还要转播这条交易的库存信息。

  之后将这个交易放入工作栈中,从孤儿交易(溯源失败的交易)池中递归检验所有依赖这个交易的孤儿交易,成功则删除该孤儿交易(这个交易不再孤儿了)。如果这笔交易不能被接受,则将该交易放入孤儿交易池mapOrphanTransactions中。

else if (strCommand == "review")
{
 CDataStream vMsg(vRecv);
 CReview review;
 vRecv >> review;
 CInv inv(MSG_REVIEW, review.GetHash());
 pfrom->AddInventoryKnown(inv)
 if (review.AcceptReview())
 {
 RelayMessage(inv, vMsg);
 mapAlreadyAskedFor.erase(inv);
 }
}

  如果命令字符串的内容是”review”,则是审查消息,将数据反序列化到CReview结构体中,如果这个审查消息的的检查是正确的,则放入待转发消息池。

else if (strCommand == "block")
{
 auto_ptr<CBlock> pblock(new CBlock);
 vRecv >> *pblock;
 
 CInv inv(MSG_BLOCK, pblock->GetHash());
 pfrom->AddInventoryKnown(inv);// 增加库存
 
 if (ProcessBlock(pfrom, pblock.release()))
 mapAlreadyAskedFor.erase(inv);
}

  如果命令字符串的内容是”block”,则代表是块消息。首先将数据放入到一个CBlock结构体中。首先要把块类型库存存入节点库存表,然后处理块。

  处理的主要过程是先检查块的格式,包含的交易是否正确。然后看这个块的前置块的索引是否在本地查得到,查不到就作为孤儿块保存,并向发来该块的节点请求该块的前置块数据。如果这个块的前置块可以在块索引中找到,但是该块的块索引却不存在,则存储该块到数据库。

else if (strCommand == "getaddr")
{
 pfrom->vAddrToSend.clear();
 //// need to expand the time range if not enough found
 int64 nSince = GetAdjustedTime() - 60 * 60; // in the last hour 往前推一个小时
 CRITICAL_BLOCK(cs_mapAddresses)
 {
 foreach(const PAIRTYPE(vector<unsigned char>, CAddress)& item, mapAddresses)
 {
 if (fShutdown)
 return true;
 const CAddress& addr = item.second;
 if (addr.nTime > nSince)
 pfrom->vAddrToSend.push_back(addr);
 }
 }
}

  如果命令字符串的内容是”getaddr”,则代表是请求地址信息。遍历自己的地址表,将时间为一个小时内的地址信息保存该节点的待发送地址池,后续线程会将这个地址池的地址发出去给实体节点(本地节点只是实体的映射)。

else if (strCommand == "checkorder")
{
 uint256 hashReply;
 CWalletTx order;
 vRecv >> hashReply >> order;
 
 /// we have a chance to check the order here
 
 // Keep giving the same key to the same ip until they use it
 if (!mapReuseKey.count(pfrom->addr.ip))
 mapReuseKey[pfrom->addr.ip] = GenerateNewKey();
 
 // Send back approval of order and pubkey to use
 CScript scriptPubKey;
 scriptPubKey << mapReuseKey[pfrom->addr.ip] << OP_CHECKSIG;
 pfrom->PushMessage("reply", hashReply, (int)0, scriptPubKey);
}

  如果命令字符串的内容是”checkorder”,将数据反序列放入hashReply和order,如果mapReuseKey里未存放过该地址,则创建一个新的钥匙对,并将公钥填入mapReuseKey表,之后以该公钥创建一个公钥脚本,推送reply消息给发来消息的节点。

 else if (strCommand == "submitorder")
{
 uint256 hashReply;
 CWalletTx wtxNew;
 vRecv >> hashReply >> wtxNew;
 // Broadcast
 if (!wtxNew.AcceptWalletTransaction())
 {
 pfrom->PushMessage("reply", hashReply, (int)1);
 return error("submitorder   AcceptWalletTransaction() failed, returning error 1");
 }
 wtxNew.fTimeReceivedIsTxTime = true;
 AddToWallet(wtxNew);
 wtxNew.RelayWalletTransaction();
 mapReuseKey.erase(pfrom->addr.ip);
 
 // Send back confirmation
 pfrom->PushMessage("reply", hashReply, (int)0);
}

  如果命令字符串的内容是”submitorder”,将数据反序列放入hashReply和wtxNew,判断这条钱包交易是否能够接受,可以则推送正确的确认信息,并将交易加入钱包再转发。不然推送错误的确认信息。

else if (strCommand == "reply")
{
 uint256 hashReply;
 vRecv >> hashReply;
 
 CRequestTracker tracker;
 CRITICAL_BLOCK(pfrom->cs_mapRequests)
 {
 map<uint256, CRequestTracker>::iterator mi = pfrom->mapRequests.find(hashReply);
 if (mi != pfrom->mapRequests.end())
 {
 tracker = (*mi).second;
 pfrom->mapRequests.erase(mi);
 }
 }
 if (!tracker.IsNull())
 tracker.fn(tracker.param1, vRecv);
}

  如果命令字符串的内容是”reply”,将数据反序列放入hashReply,代表确认消息。就是对自己之前发送消息的确认,如果在自己的消息请求表里找到该确认消息的hash,代表该请求已被回应,擦除该消息请求。

2.3 发送消息

  下面是消息发送函数SendMessages内容的解析:

vector<CAddress> vAddrToSend;
vAddrToSend.reserve(pto->vAddrToSend.size());
// 如果发送的地址不在已知地址的集合中,则将其放入临时地址发送数组中
foreach(const CAddress& addr, pto->vAddrToSend)
 if (!pto->setAddrKnown.count(addr))
 vAddrToSend.push_back(addr);
pto->vAddrToSend.clear();
// 如果临时地址发送数组不为空,则进行地址的消息的发送
if (!vAddrToSend.empty())
 pto->PushMessage("addr", vAddrToSend);}

  定义一个临时的待发送地址池,遍历节点的待发送地址中的地址,判断这些地址是不是在该节点的已知地址集合中,是则将该地址放入临时定义的待发送地址池vAddrToSend。然后清空节点的待发送地址池。如果临时的待发送地址池非空,则向该节点推送addr消息。

vector<CInv> vInventoryToSend;
CRITICAL_BLOCK(pto->cs_inventory)
{
 vInventoryToSend.reserve(pto->vInventoryToSend.size());
 foreach(const CInv& inv, pto->vInventoryToSend)
 {
 // returns true if wasn't already   contained in the set
 if (pto->setInventoryKnown.insert(inv).second)
 vInventoryToSend.push_back(inv);
 }
 pto->vInventoryToSend.clear();
 pto->setInventoryKnown2.clear();
}
// 库存消息发送
if (!vInventoryToSend.empty())
 pto->PushMessage("inv", vInventoryToSend);

  定义一个临时的待发送库存池,与上面类似,遍历该节点的待发送库存池,如果这条库存不是该节点的已知库存,则将库存推送到临时待发送库存池。之后清空节点的待发送库存池和已知库存2池。这之后如果临时带发送库存池不是空的,则需要向节点推送库存inv消息。

vector<CInv> vAskFor;
int64 nNow = GetTime() * 1000000;
CTxDB txdb("r");
// 判断节点对应的请求消息map是否为空,且对应的请求map中的消息对应的最早请求时间是否小于当前时间
while (!pto->mapAskFor.empty() && (*pto->mapAskFor.begin()).first <= nNow)
{
 const CInv& inv = (*pto->mapAskFor.begin()).second;
 printf("sending getdata: %s\n", inv.ToString().c_str());
 if (!AlreadyHave(txdb, inv))
 vAskFor.push_back(inv);// 不存在才需要进行消息发送
 pto->mapAskFor.erase(pto->mapAskFor.begin());// 请求消息处理完一条就删除一条
}
if (!vAskFor.empty())
 pto->PushMessage("getdata", vAskFor);
 
}

  创建一个临时的请求数据列表,以库存来请求具体数据,遍历该节点的库存请求列表,逐个判断,如果库存的内容在数据库中不存在,则将这条库存放入临时的库存请求列表。这之后向该节点推送请求数据消息getdata。节点实体接收到该消息后将会根据库存hash和类型返回相应信息。

  消息处理处理部分结束。

  review,checkorder与submitorder三个消息似乎是为非全节点准备的,因为看内容上来说是把一些工作托付给其他节点完成,说明这些工作自己完成不了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章