现在我们来修复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 fDiscount=false) 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. 查找并处理依赖当前区块的孤块 ==================== // 创建工作队列,用于保存已经接受、需要继续向后处理的区块哈希 // 通过工作队列,可以逐步处理依赖链上的孤块 vector<uint256> 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 (multimap<uint256, 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 don't 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 we're 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 //vector<uint256> vWorkQueue; //vWorkQueue.push_back(hash); //for (int i = 0; i < vWorkQueue.size(); i++) //{ // uint256 hashPrev = vWorkQueue[i]; // for (multimap<uint256, 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循环遍历它呢,此时它只有一个元素啊。这是为了,如果你添加的儿子区块,如果他也有一个儿子孤块呢,所以这里就会把,比如说1,2,3区块,也会把2区块哈希添加到vWorkQueue里,所以继续寻找。
但是这里有个问题,2号区块,被添加到孤块时,他必定是通过验证了,后续他也会处理他的儿子区块,为什么在这里还要往下遍历2号区块的儿子块呢。
这是因为:比如说,2号区块最先到,然后才是3号区块,最后才是1号区块。
当然实际是,如果2号区块先到,那么它没有父区块,此时只能被做为孤块存储。
那么3号区块到了,他同样在mapBlockIndex里找不到2号区块。
所以处理逻辑是,不管有多少孤块,必须是先处理先写入链上的那个区块的下一个区块。
然后才是儿子孙子区块,这样的按顺序的一个个处理写入。
8.多个儿子
我们来看一下,根据这个区块的哈希,然后从mapOrphanBlocksByPrev找它的儿子块,是怎么查找的。先来看for循环遍历:
for (multimap<uint256, 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就是循环语句,就该干它原本的工作,这样便于阅读。
为了便于阅读,我们可以改成这样的写法:
// 第一步:找到父区块哈希对应的孤块范围起点 multimap<uint256, CBlock*>::iterator mi = mapOrphanBlocksByPrev.lower_bound(hashPrev); // 第二步:找到这个范围的终点 multimap<uint256, 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-1,125-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 doesn't 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 don't 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 临时注释 //map<uint256, 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() : block's 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 // vector<unsigned char> vchPubKey; // if (ExtractPubKey(vtx[0].vout[0].scriptPubKey, false, vchPubKey)) // { // unsigned short nAtom = GetRand(USHRT_MAX - 100) + 100; // vector<unsigned 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 临时注释 map<uint256, 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() : block's 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函数了。
这个函数包含最优链和分叉的处理。
在之前的章节中,我们让这个函数只处理创世块,接下来,我们要让它处理最优链,取消一部分注释,就是这个区块正好接到最优链上面去,让我们的第二个区块能上链,并写索引到数据库,然后更新最优链为我们的第二个区块,记录一些数据。
篇幅所限,下一章说明。