现在我们来修复BitcoinMiner里的代码1.IsFinal这个函数未定义这个函数是属于CTransaction类我们在main.h相类里定义一下就行代码如下bool IsFinal() const { if (nLockTime 0 || nLockTime nBestHeight) return true; foreach(const CTxIn txin, vin) if (!txin.IsFinal()) return false; return true; }这个函数大概就是可以设定一笔交易满足什么时间或区块高才能上链。也就是说我可以规定这笔交易目前不上链等达到我的要求才能被区块接收。具体是什么样需要看代码设计。这里我们只要修复就行因为现在默认都是即时上链。可以无视返回的都是true。将此代码复制进去然后将foreach循环改成for的写法。2.CTxIn IsFinal然后下级IsFinal又报未定义我们去maih.h里在CTxIn类里添加如下代码就行bool IsFinal() const { return (nSequence UINT_MAX); }好IsFinal函数未定义问题已解决。3.GetMinFee接下来看CTransaction类的第二个函数GetMinFee未定义这个函数之前提到过。就是获取这笔tx所需要的手续费根据系统设定)。我们去main.h里定义int64 GetMinFee(bool fDiscountfalse) const { unsigned int nBytes ::GetSerializeSize(*this, SER_NETWORK); if (fDiscount nBytes 10000) return 0; return (1 (int64)nBytes / 1000) * CENT; }然后CENT未定义同样在maih.h里定义static const int64 CENT 1000000;好GetMinFee问题已解决。4.GetBlockValue这个函数属于CBlock我们需要在main.h里的CBlock类里声明一下int64 GetBlockValue(int64 nFees) const;然后再到main.cpp里相关处添加定义即这部分后面////////////////////////////////////////////////////////////////////////////// // // CBlock and CBlockIndex //int64 CBlock::GetBlockValue(int64 nFees) const { int64 nSubsidy 50 * COIN; // Subsidy is cut in half every 4 years nSubsidy (nBestHeight / 210000); return nSubsidy nFees; }5.ProcessBlock好现在还有最后一个函数报错--ProcessBlock未定义。但这个函数涉及的功能比较多之前我们也看过调用链就单向调用了非常多的函数还没有算上分支上的函数。所以我们具体看一下如果需要修复的下级函数过多就是跟目前不相关的功能太多我们需要注释掉。或者将需要的功能单独提出来写一个函数。剩下的等正常交易时解决一部分代码然后恢复网络功能时解决另一部分问题。这个函数暂时不急着修复我们先来看一下。由于我们挖到矿后就是第二个区块生成我们需要区块上链写入数据库构建索引维护各种表以及最优链设置等都得通过ProcessBlock函数实现所以这个函数不能直接注释掉。我们来看一下这个函数据具体代码// 处理接收到的区块 // pfrom发送该区块的节点如果区块不是通过网络节点接收的可以为 NULL // pblock待处理的区块指针 // 返回值true 表示该区块被正常处理或被成功暂存为孤块 // false 表示处理过程中出现错误 bool ProcessBlock(CNode* pfrom, CBlock* pblock) { // 1. 检查区块是否重复 // 计算当前区块的哈希值作为区块的唯一标识 uint256 hash pblock-GetHash(); // 检查当前区块是否已经存在于主区块索引表中 // mapBlockIndex 保存已经建立索引的区块信息 if (mapBlockIndex.count(hash)) // 如果已经存在说明该区块之前已经处理过 // 输出错误信息区块高度、区块哈希前14个字符 // 并返回 false return error(ProcessBlock() : already have block %d %s, mapBlockIndex[hash]-nHeight, hash.ToString().substr(0,14).c_str()); // 检查当前区块是否已经存在于孤块集合中 // mapOrphanBlocks 保存缺少前序区块、暂时无法接入的区块 if (mapOrphanBlocks.count(hash)) // 如果该区块已经作为孤块保存就不再重复处理 // 输出错误信息并返回 false return error(ProcessBlock() : already have block (orphan) %s, hash.ToString().substr(0,14).c_str()); // 2. 进行初步合法性检查 // 调用区块自身的检查函数检查区块的基本合法性 // 例如区块大小、交易数量、交易结构、Merkle Root、 // 工作量证明PoW等具体规则取决于该版本的 CheckBlock() 实现 if (!pblock-CheckBlock()) { // 区块未通过初步检查释放动态分配的区块对象 delete pblock; // 返回错误表示区块检查失败 return error(ProcessBlock() : CheckBlock FAILED); } // 3. 检查前一个区块是否存在 // 检查当前区块所引用的前一个区块是否已存在于区块索引表 // hashPrevBlock 表示当前区块的前序区块哈希 if (!mapBlockIndex.count(pblock-hashPrevBlock)) { // 找不到前序区块说明当前区块暂时无法连接到已知区块链 // 因此将它视为孤块Orphan Block // 打印其前序区块哈希的前14个字符方便调试 printf(ProcessBlock: ORPHAN BLOCK, prev%s\n, pblock-hashPrevBlock.ToString().substr(0,14).c_str()); // 将当前区块保存到孤块映射表中 // key 是当前区块哈希value 是区块对象指针 // 这样后续可以根据区块哈希查找这个孤块 mapOrphanBlocks.insert(make_pair(hash, pblock)); // 同时按前序区块哈希建立索引 // key 是缺失的前序区块哈希value 是孤块指针 // 这样当缺失的前序区块到达时可以快速找到依赖它的孤块 mapOrphanBlocksByPrev.insert( make_pair(pblock-hashPrevBlock, pblock) ); // 如果该区块来自网络节点就向发送方请求相关区块数据 if (pfrom) // 发送 getblocks 消息请求从当前最佳区块链的位置开始 // 获取到孤块根节点附近所需的区块 // CBlockLocator(pindexBest)根据当前最佳链构造区块定位器 // GetOrphanRoot(pblock)找到该孤块链上最早的孤块 // 目的是补齐缺失的前序区块 pfrom-PushMessage( getblocks, CBlockLocator(pindexBest), GetOrphanRoot(pblock) ); // 孤块已经成功暂存等待依赖的区块到达 // 这里返回 true 并不代表它已经正式接入区块链 return true; } // 4. 接受并保存当前区块 // 前序区块已存在尝试正式接受当前区块 // AcceptBlock() 通常还会进行上下文相关的验证 // 并在验证通过后将区块写入磁盘、建立区块索引等 if (!pblock-AcceptBlock()) { // 接受失败释放当前区块对象 delete pblock; // 返回错误表示区块未能通过正式接受流程 return error(ProcessBlock() : AcceptBlock FAILED); } // 当前区块已被接受释放本函数持有的区块对象 // 注意这里假设 AcceptBlock() 已经完成必要的数据保存 // 不再需要调用者继续持有这个 pblock 对象 delete pblock; // 5. 查找并处理依赖当前区块的孤块 // 创建工作队列用于保存已经接受、需要继续向后处理的区块哈希 // 通过工作队列可以逐步处理依赖链上的孤块 vectoruint256 vWorkQueue; // 将刚刚接受的区块哈希放入队列 // 以它为起点寻找那些以前因缺少它而被暂存的孤块 vWorkQueue.push_back(hash); // 遍历工作队列 // 当处理某个区块后如果又成功接受了新的孤块 // 就把新孤块的哈希加入队列继续处理下一层依赖 for (int i 0; i vWorkQueue.size(); i) { // 取出当前正在处理的区块哈希 // 它可能是最初接受的区块也可能是后来接受的孤块 uint256 hashPrev vWorkQueue[i]; // 在 mapOrphanBlocksByPrev 中查找 // 所有以前序区块哈希 hashPrev 为键的孤块 // lower_bound(hashPrev) 指向该键对应范围的起点 // upper_bound(hashPrev) 指向该键对应范围的终点 // 因为这是 multimap所以同一个前序区块可以对应多个孤块 for (multimapuint256, CBlock*::iterator mi mapOrphanBlocksByPrev.lower_bound(hashPrev); mi ! mapOrphanBlocksByPrev.upper_bound(hashPrev); mi) { // 取得依赖当前前序区块的孤块对象 CBlock* pblockOrphan (*mi).second; // 尝试正式接受这个孤块 // 此时它的直接前序区块已经被接受 // 因此可以继续进行上下文验证和区块接入流程 if (pblockOrphan-AcceptBlock()) // 如果接受成功将该孤块的哈希加入工作队列 // 后续继续查找以这个新区块为前序区块的其他孤块 vWorkQueue.push_back(pblockOrphan-GetHash()); // 无论 AcceptBlock() 是否成功都将该孤块 // 从按区块哈希索引的孤块表中移除 mapOrphanBlocks.erase(pblockOrphan-GetHash()); // 释放孤块对象占用的内存 // 注意即使 AcceptBlock() 失败这里也会删除该孤块 // 不会继续保留它等待重试 delete pblockOrphan; } // 当前前序区块对应的孤块已经全部处理完毕 // 删除 mapOrphanBlocksByPrev 中以 hashPrev 为键的所有记录 // 避免留下无用的孤块索引 mapOrphanBlocksByPrev.erase(hashPrev); } // 6. 输出成功信息并返回 // 打印日志表示本次区块处理流程已经完成 printf(ProcessBlock: ACCEPTED\n); // 返回 true表示当前区块已被接受 // 并且本次发现的相关孤块也已尝试处理完毕 return true; }这个函数一大部分涉及到孤块处理它的处理逻辑有点多我们看情况可以注释掉因为我们是单节点不存在从从他节点接收区块也不存在什么网络延迟这种问题。所以我们可以选择注释掉。6.收集孤块我们先把添加孤块的部分注释掉就是在正常索引中找不到这个块的父块就把它添加孤块数组中/* //If dont already have its previous block, shunt it off to holding area until we get it if (!mapBlockIndex.count(pblock-hashPrevBlock)) { printf(ProcessBlock: ORPHAN BLOCK, prev%s\n, pblock-hashPrevBlock.ToString().substr(0, 14).c_str()); mapOrphanBlocks.insert(make_pair(hash, pblock)); mapOrphanBlocksByPrev.insert(make_pair(pblock-hashPrevBlock, pblock)); Ask this guy to fill in what were missing if (pfrom) pfrom-PushMessage(getblocks, CBlockLocator(pindexBest), GetOrphanRoot(pblock)); return true; }*/孤块集:mapOrphanBlocks另一个mapOrphanBlocksByPrev是关联这个孤块的父哈希。即给这个父哈希关联它的儿子。即方便点在于下次接到一个块后可以直接查这个看看这个块有没有儿子。7.处理孤块是怎么处理孤块的呢比如现在来了一个区块已经通过验证呢那就寻找这个块有没有儿子就是在孤块集里寻找。//// Recursively process any orphan blocks that depended on this one //vectoruint256 vWorkQueue; //vWorkQueue.push_back(hash); //for (int i 0; i vWorkQueue.size(); i) //{ // uint256 hashPrev vWorkQueue[i]; // for (multimapuint256, CBlock*::iterator mi mapOrphanBlocksByPrev.lower_bound(hashPrev); // mi ! mapOrphanBlocksByPrev.upper_bound(hashPrev); // mi) // { // CBlock* pblockOrphan (*mi).second; // if (pblockOrphan-AcceptBlock()) // vWorkQueue.push_back(pblockOrphan-GetHash()); // mapOrphanBlocks.erase(pblockOrphan-GetHash()); // delete pblockOrphan; // } // mapOrphanBlocksByPrev.erase(hashPrev); //}这部分可以注释掉可以看到先把这个块的哈希加到vWorkQueue里。但是后面为什么要用for循环遍历它呢此时它只有一个元素啊。这是为了如果你添加的儿子区块如果他也有一个儿子孤块呢所以这里就会把比如说123区块也会把2区块哈希添加到vWorkQueue里所以继续寻找。但是这里有个问题2号区块被添加到孤块时他必定是通过验证了后续他也会处理他的儿子区块为什么在这里还要往下遍历2号区块的儿子块呢。这是因为比如说2号区块最先到然后才是3号区块最后才是1号区块。当然实际是如果2号区块先到那么它没有父区块此时只能被做为孤块存储。那么3号区块到了他同样在mapBlockIndex里找不到2号区块。所以处理逻辑是不管有多少孤块必须是先处理先写入链上的那个区块的下一个区块。然后才是儿子孙子区块这样的按顺序的一个个处理写入。8.多个儿子我们来看一下根据这个区块的哈希然后从mapOrphanBlocksByPrev找它的儿子块是怎么查找的。先来看for循环遍历:for (multimapuint256, CBlock*::iterator mi mapOrphanBlocksByPrev.lower_bound(hashPrev); mi ! mapOrphanBlocksByPrev.upper_bound(hashPrev); mi)8.1lower_bound这个函数就是根据提供的参数也就是hashPrev找到第一个大于或等于该值的元素返回迭代器(注意按排序的来)。而upper_bound就是它返回大于hashPrev的第一个元素的迭代器。比如1,2,5,5,5,6,7 找的值是5那么lower_bound函数返回的是第三个5而upper_bound大于5的第一个元素是6即返回6那个迭代器。其实我不太喜欢在for里面写很多的算法表达式之类的我认为for就是循环语句就该干它原本的工作这样便于阅读。为了便于阅读我们可以改成这样的写法// 第一步找到父区块哈希对应的孤块范围起点 multimapuint256, CBlock*::iterator mi mapOrphanBlocksByPrev.lower_bound(hashPrev); // 第二步找到这个范围的终点 multimapuint256, CBlock*::iterator miEnd mapOrphanBlocksByPrev.upper_bound(hashPrev); // 第三步遍历所有以 hashPrev 为父哈希的孤块 for (; mi ! miEnd; mi) { // 取出当前孤块指针 CBlock* pblockOrphan (*mi).second; // 后续处理逻辑…… }可以看到这是范围查找说明一个区块可能同时有多个儿子块分叉了我们都需要处理。处理代码很简单把这个儿子块写入一下数据库上链8.1添加儿子块if (pblockOrphan-AcceptBlock())如果成功了则将它添加到vWorkQueue里可以把它当成一个父块了继续找它还有没有儿子孤块8.2找儿子块的后代vWorkQueue.push_back(pblockOrphan-GetHash());回到上一步然后不管成不成功都会从孤块中把这个儿子块删掉。为什么因为你已经找到你的父块了还添加不上那已经没必要了说明有问题8.3删除儿子块mapOrphanBlocks.erase(pblockOrphan-GetHash()); delete pblockOrphan;8.4处理mapOrphanBlocksByPrev当此区块的关联的孤块全部处理完毕后就可以从关联中删除掉了后续将不在有此区块儿子孤块产生因为此区块已经上链了。mapOrphanBlocksByPrev.erase(hashPrev);好以上就是孤块的处理逻辑注意这里有一些点需要说明如果是有多个儿子块那么acceptblock都接收了最优链是在哪里选择的呢在acceptblock函数中后面会调用AddToBlockIndex函数这个函数会处理这些问题。相关情况看该函数代码即可。好我们再来汇总一下这个孤块处理逻辑首先这个块能被寻找儿子块那必须得是自己不能是孤块如果自己是孤块在前面的代码已经被return true了。也就是说此时这个块它的所有上级都是有的。然后我们就在这个块里从孤块集里找出它能拼凑出的最长块。并且可以是多条。比如现在这真块是第123个它就会找第124再找125。如果有就会直接找下去。包括分支124-1125-1。 也是一样。这个逻辑确保了工作不会出错不会有遗漏不会说。比如说现在是第125个区块没有父块我把它添加到孤块里然后126区块来了也没有父块没写入数据库接着124区块来了然后找它的儿子块找到是第125个。把125的区块加进去。但却不找125区块的儿子块126上不了链。然后是127区块来了接着后面一直出错都是孤块了第126的那个区块没有逻辑去处理它了。好写了这么多关于孤块说明是即为了后续的学习做准备又知道了该怎么注释代码。我们可以把涉及到所有孤块的代码都注释掉在这里是没问题的。9.修复函数那么此时后面ProcessBlock就剩下两个功能分别是验证区块CheckBlock,和接收区块AcceptBlock。把这两个函数修复好后ProcessBlock也就是修复完成我们就可以创建第二个真实区块了。10.CheckBlock这个函数就是检测一下区块的基本规则是不是合法比如区块大小区块的时间难度符不符合要求等。还有矿工是不伪造多笔coinbase。我们先添加CheckBlock函数在CBlock类中main.cpp 和main.h分别添加定义和声明。此函数代码如下bool CBlock::CheckBlock() const { // These are checks that are independent of context // that can be verified before saving an orphan block. // Size limits if (vtx.empty() || vtx.size() MAX_SIZE || ::GetSerializeSize(*this, SER_DISK) MAX_SIZE) return error(CheckBlock() : size limits failed); // Check timestamp if (nTime GetAdjustedTime() 2 * 60 * 60) return error(CheckBlock() : block timestamp too far in the future); // First transaction must be coinbase, the rest must not be if (vtx.empty() || !vtx[0].IsCoinBase()) return error(CheckBlock() : first tx is not coinbase); for (int i 1; i vtx.size(); i) if (vtx[i].IsCoinBase()) return error(CheckBlock() : more than one coinbase); // Check transactions for(const CTransaction tx: vtx) if (!tx.CheckTransaction()) return error(CheckBlock() : CheckTransaction failed); // Check proof of work matches claimed amount if (CBigNum().SetCompact(nBits) bnProofOfWorkLimit) return error(CheckBlock() : nBits below minimum work); if (GetHash() CBigNum().SetCompact(nBits).getuint256()) return error(CheckBlock() : hash doesnt match nBits); // Check merkleroot if (hashMerkleRoot ! BuildMerkleTree()) return error(CheckBlock() : hashMerkleRoot mismatch); return true; }将foreach改成for的写法后只有一个报错if (!tx.CheckTransaction())这个函数未定义这是更细致的一层检查每笔tx检查一下。我们在main.h里的类定义一下bool CheckTransaction() const { // Basic checks that dont depend on any context if (vin.empty() || vout.empty()) return error(CTransaction::CheckTransaction() : vin or vout empty); // Check for negative values foreach(const CTxOut txout, vout) if (txout.nValue 0) return error(CTransaction::CheckTransaction() : txout.nValue negative); if (IsCoinBase()) { if (vin[0].scriptSig.size() 2 || vin[0].scriptSig.size() 100) return error(CTransaction::CheckTransaction() : coinbase script size); } else { foreach(const CTxIn txin, vin) if (txin.prevout.IsNull()) return error(CTransaction::CheckTransaction() : prevout is null); } return true; }改一下foreach即可至此checkblock函数已经不报错了。11.AcceptBlock接下来是AcceptBlock函数这个函其实并没有报错之前已经定义过但是缩减版的注释了一些代码现在我们得恢复注释部分的功能来看一下之前的注释bool CBlock::AcceptBlock() { // Check for duplicate uint256 hash GetHash(); if (mapBlockIndex.count(hash)) return error(AcceptBlock() : block already in mapBlockIndex); //// Get prev block index 临时注释 //mapuint256, CBlockIndex*::iterator mi mapBlockIndex.find(hashPrevBlock); //if (mi mapBlockIndex.end()) // return error(AcceptBlock() : prev block not found); //CBlockIndex* pindexPrev (*mi).second; //// Check timestamp against prev //if (nTime pindexPrev-GetMedianTimePast()) // return error(AcceptBlock() : blocks timestamp is too early); //// Check proof of work //if (nBits ! GetNextWorkRequired(pindexPrev)) // return error(AcceptBlock() : incorrect proof of work); // Write block to history file unsigned int nFile; unsigned int nBlockPos; if (!WriteToDisk(!fClient, nFile, nBlockPos)) return error(AcceptBlock() : WriteToDisk failed); if (!AddToBlockIndex(nFile, nBlockPos)) return error(AcceptBlock() : AddToBlockIndex failed); //if (hashBestChain hash) // RelayInventory(CInv(MSG_BLOCK, hash)); // // Add atoms to user reviews for coins created // vectorunsigned char vchPubKey; // if (ExtractPubKey(vtx[0].vout[0].scriptPubKey, false, vchPubKey)) // { // unsigned short nAtom GetRand(USHRT_MAX - 100) 100; // vectorunsigned short vAtoms(1, nAtom); // AddAtomsAndPropagate(Hash(vchPubKey.begin(), vchPubKey.end()), vAtoms, true); // } return true; }12.混乱报错PS目前先打断一下现在虽然processblock函数不报错但main.h头文件里的代码全报错报各种类型未定义。之前没这些错的main.h头文件也没问题的。怎么添加个ProcessBlock函数出了这么多问题因为main.h里的类型依赖的头文件全靠其它cpp包含它时包含的所以如果此时新建了一个cpp只包含main.h那么main.h里面就会报错但此时它并没有告诉具体是哪个cpp。而我也没新添加的cpp。最后我发现将main.h里的CheckTransaction函数注释掉错误就会少很多但这个函数并没有语法错误啊。我们来单独一个一个编译cpp方式。先右键net.cpp尝试单独编译然后报什么CheckTransaction不是CTransaction的函数以及报CheckTransaction函数里的CTxOut类型不存在大概是这些吧。有些无厘头。我记不太清了因为我重启编译器后这些错误都消失了只有两个真正的报错。就是ProcessBlock和GetNextWorkRequired找不到标识符。这是因为我将这个两个函数的定义写在调用它的BitcoinMiner后面导致这里我们不采取声明办法解决。还是按源码中来将BitcoinMiner函数的定义直接剪切到main.cpp里的最后面就行。好ProcessBlock已经不报错了GetNextWorkRequired还是一样。点击进去是AcceptBlock函数将GetNextWorkRequired函数的定义放在AcceptBlock之前就行。我猜想可能是改了代码然后一些缓存还没更新不同步了然后导致的混乱报错重启一下同步。就能找到真正的报错。问题解决就行。就不细究验证了。至此编译正常----------------end-------13.取消注释我们回到AcceptBlock函数这部分注释可以直接取消://// Get prev block index 临时注释 mapuint256, CBlockIndex*::iterator mi mapBlockIndex.find(hashPrevBlock); if (mi mapBlockIndex.end()) return error(AcceptBlock() : prev block not found); CBlockIndex* pindexPrev (*mi).second; // Check timestamp against prev if (nTime pindexPrev-GetMedianTimePast()) return error(AcceptBlock() : blocks timestamp is too early); // Check proof of work if (nBits ! GetNextWorkRequired(pindexPrev)) return error(AcceptBlock() : incorrect proof of work);涉及的相关变量和函数之前都已经定义解决了。取消注释后直接就能使用了。好还有另一部分我查了下源码在源码中后半部分本来就是注释掉一个已废弃的「信誉/评论市场系统」market.h/market.cpp。我们保持原样即可然后把这个取消注释即可if (hashBestChain hash) RelayInventory(CInv(MSG_BLOCK, hash));这里注不注释都没有影响这个功能是当这个块做为我的最优链时就会广播一下给其它节点。因为我没有其它节点所以即使调用了里面的代码也不会执行。因为节点是空的。所以我们可以取消注释那么至此AcceptBlock里的所有代码恢复完毕跟源码中一模一样。好AcceptBlock第一层已经处理完成现在就剩AcceptBlock调用的AddToBlockIndex函数了。这个函数包含最优链和分叉的处理。在之前的章节中我们让这个函数只处理创世块接下来我们要让它处理最优链取消一部分注释就是这个区块正好接到最优链上面去让我们的第二个区块能上链并写索引到数据库然后更新最优链为我们的第二个区块记录一些数据。篇幅所限下一章说明。