题外话,其实我觉得源码中,除了区块链CBlock数据,另一个我们需要比较重视的就是这套索引系统。
就是CBlockIndex和AddToBlockIndex还有CTxIndex等相关,相当于一个Block的副本,并且很多验证也是直接走的索引。
这个也具有权威性,因为它本身就是基本已经验证的区块数据生成的。
也具有相同的共识性。不可篡改性。你改了自己的索引,别的节点验证对不上。也是没用的。
比如,我将自己索引记录的已花费改成未花费。然后你引用了,是行不通的。别的节点会访问自己的索引验证,结果不一致,拒绝。
1.AddToBlockIndex
当前区块高度比最优链高,并且它的父区块哈希正好是最优链的最后一个区块,那说明此区块就是最优链上的下一个区块,就是如下代码处理逻辑:
else if (hashPrevBlock == hashBestChain) { // Adding to current best branch if (!ConnectBlock(txdb, pindexNew) || !txdb.WriteHashBestChain(hash)) { txdb.TxnAbort(); pindexNew->EraseBlockFromDisk(); mapBlockIndex.erase(pindexNew->GetBlockHash()); delete pindexNew; return error("AddToBlockIndex() : ConnectBlock failed"); } txdb.TxnCommit(); pindexNew->pprev->pnext = pindexNew; // Delete redundant memory transactions foreach(CTransaction& tx, vtx) tx.RemoveFromMemoryPool(); }所以我们需要把AddToBlockIndex里的这部分代码取消注释。
2.RemoveFromMemoryPool
当区块上链成功后,那么此区块的所有tx,如果存在内存池中,那是不是可以删除掉了。
就是对应在mapTransactions里的交易。我这里第二个区块只包含coinbase,不走mapTransactions,所以为了省事。
这里的这个操作直接注释掉:
//// Delete redundant memory transactions //for(CTransaction & tx: vtx) // tx.RemoveFromMemoryPool();3.ConnectBlock
好,这里的代码不报错,但我们还得修复ConnectBlock里的功能。
我们先来看没注释部分的代码:
//// issue here: it doesn't know the version unsigned int nTxPos = pindex->nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK) - 1 + GetSizeOfCompactSize(vtx.size()); map<uint256, CTxIndex> mapUnused; int64 nFees = 0; for(CTransaction & tx:vtx) { CDiskTxPos posThisTx(pindex->nFile, pindex->nBlockPos, nTxPos); nTxPos += ::GetSerializeSize(tx, SER_DISK); if (!tx.ConnectInputs(txdb, mapUnused, posThisTx, pindex->nHeight, nFees, true, false)) return false; }代码开始,就是计算获取这个区块的每笔tx在硬盘上的位置,记录在CDiskTxPos,有的人可能会有疑惑,这个区块还没正式上链啊,怎么就已经写入硬盘吗?
实际数据确实会先写入,索引数据后写入。请看AcceptBlock的调用代码:
// 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");是已经调用了WriteToDisk函数。然后才是AddToBlockIndex。
好,回到上面,在里面获取了tx的位置干嘛呢,是给ConnectInputs函数用的,第三个参数posThisTx:
if (!tx.ConnectInputs(txdb, mapUnused, posThisTx, pindex->nHeight, nFees, true, false)) return false;因为这个函数需要这条交易的位置。然后在里面,如下:
// Mark outpoints as spent txindex.vSpent[prevout.n] = posThisTx;就是为了给已花费做标记用的,让被引用交易知道是谁花了它的钱,这笔tx在硬盘上的位置。
关于ConnectInputs就不过多的说明了,之前也讲过。
这样每条交易都ConnectInputs过后,就证明每条交易的引用,脚本验证啥的都是合法的。
接下来,我们来看注释部分的代码:
if (vtx[0].GetValueOut() > GetBlockValue(nFees)) return false; // Update block index on disk without changing it in memory. // The memory index structure will be changed after the db commits. if (pindex->pprev) { CDiskBlockIndex blockindexPrev(pindex->pprev); blockindexPrev.hashNext = pindex->GetBlockHash(); txdb.WriteBlockIndex(blockindexPrev); } // Watch for transactions paying to me foreach(CTransaction & tx, vtx) AddToWalletIfMine(tx, this);4.GetValueOut
就是计算coinbase的vout,不能超过规定的费用,奖励加手续费。
GetValueOut在main.h里的CTransaction类中定义一下,代码如下:
int64 GetValueOut() const { int64 nValueOut = 0; for(const CTxOut & txout:vout) { if (txout.nValue < 0) throw runtime_error("CTransaction::GetValueOut() : negative value"); nValueOut += txout.nValue; } return nValueOut; }5.父区块索引更新
if (pindex->pprev) { CDiskBlockIndex blockindexPrev(pindex->pprev); blockindexPrev.hashNext = pindex->GetBlockHash(); txdb.WriteBlockIndex(blockindexPrev); }然后将父区块索引更新一下,就是hashNext,表明有儿子了,登记一下。更新到数据库里去。
6.AddToWalletIfMine
然后检查这笔tx如果跟我有关,则单独把这个tx添加的我的钱包里。
由于这部分涉及的代码有点多,暂时这部也不是重点。就先注释掉了。
//// Watch for transactions paying to me //for(CTransaction & tx:vtx) // AddToWalletIfMine(tx, this);7.ConnectInputs
现在还剩最后一个函数,所以的流程就走完了,而这个函数ConnectInputs在四十二章已经讲过。
但那时并没有取消注释,现在我们取消掉所有的注释,就是跟源码中一模一样就行。
我检查了下,没有编译报错,下级调用函数也都是完善的。
好,接下来,我们就来把流程检查一遍,从ProcessBlock函数开始,没问题,我们就开始创建第二个区块。
8.只挖一次
我这里为省事,还是主线程直接调用BitcoinMiner函数,方便调试吧。
然后,在BitcoinMiner挖到第二个区块后,马上停止,不选择继续挖第三个。
所以我用在相应的地方,在break前面写上return true就行。就是在下面这部分代码里:
if (hash <= hashTarget) { ... Sleep(500); return true;// 只挖一次 临时增加 break; }9.主函数调用
主函数代码如下:
void Init2() { LoadBlockIndex(); LoadWallet(); } int main() { Init2(); fGenerateBitcoins = 1; BitcoinMiner(); }先调用LoadBlockIndex,会把我之前创建的创世块(索引)加载,然后在此基础上构建第二个区块。
我点编译后,报COINBASE_MATURITY常量未定义,这个在ConnectInputs里调用,在引用的是coinbase交易时,会需要此参数做一些判断。
我们直接在main.h里定义即可:
static const int COINBASE_MATURITY = 100;10.运行编译
我们再次编译运行,程序可以启动,但输出如下:
首先第一个,区块挖出来,如果我没记错的话,我设置的难度是前面5个0,但现在,4个0就可以了。
然后呢,AcceptBlock失败,ProcessBlock自然也失败。
AcceptBlock是报父区块未发现,也就是和创世块未对接上,我们在CBlock里,可以看到hashPrevBlock的值是一串0,也并不是创世块的哈希值。
我们先一个一个来。
11.解决错误
我们先来解决难度问题,在BitcoinMiner这里,加上后面这些判断:
unsigned int nTransactionsUpdatedLast = nTransactionsUpdated; CBlockIndex* pindexPrev = pindexBest; unsigned int nBits = GetNextWorkRequired(pindexPrev); // 输出 nBits,16 进制 printf("\n========== Mining Debug ==========\n"); printf("nBits = 0x%08x\n", nBits); // 检查 pindexBest if (pindexBest == NULL) { printf("pindexBest = NULL !!!\n"); } return true;是通过上一个区块来获取难度的,输出如下:
最优链pindexBest为null,说明创世块加载时没有和最优链绑定,我们进一步判断。是创世块没加载出来,还是其它什么原因。
好,因为pindexBest为null,所以难度为默认值,就是之前我们设定最小难度值:bnProofOfWorkLimit,我们设置的就是4个0.逻辑没有问题。
所有的问题都是创世块没加载到引发的。
12.LoadBlockIndex
我们去LoadBlockIndex一个一个输出测试。
再进入到:
bool CTxDB::LoadBlockIndex()到这里,测试一下加载到创世块了吗,在if随便输出一下即可,有输出代表成功:
// Watch for genesis block and best block if (pindexGenesisBlock == NULL && diskindex.GetBlockHash() == hashGenesisBlock) pindexGenesisBlock = pindexNew;实测有输出,已经读取到测试块了。
到这里其实已经发现问题了,因为接着后面有一部分注释的代码:
// if (!ReadHashBestChain(hashBestChain)) // { // if (pindexGenesisBlock == NULL) // return true; // return error("CTxDB::LoadBlockIndex() : hashBestChain not found\n"); // } //if (!mapBlockIndex.count(hashBestChain)) // return error("CTxDB::LoadBlockIndex() : blockindex for hashBestChain not found\n"); //pindexBest = mapBlockIndex[hashBestChain]; //nBestHeight = pindexBest->nHeight; //printf("LoadBlockIndex(): hashBestChain=%s height=%d\n", hashBestChain.ToString().substr(0, 14).c_str(), nBestHeight);难怪pindexBest没有值,取消所有的注释即可。
13.blockview
PS:在运行之前,我让AI做了个可以浏览blk0001.dat文件的小窗口程序,这个小工具,让我们能很直观的看到是否正常写入区块,以及数据是否正常。
为了便于运行,我直接让AI 用win32 API做出的窗口程序,不需要依赖其他库,可直接编译。
代码下载地址:
https://download.csdn.net/download/d3582077/93793486
vs2022选择新建项目-windows 桌面应用程序,将上面代码代替原有的即可。
效果:
用此程序打开blk0001.dat文件,可以看到我们之前写的创世块数据都对得上。
接下来,我们就可以很方便的,看代码运行的结果了。
注意,这个只能打开区块数据文件,区块索引文件是查看不了的,那个也可以做。只是需要调用BDB库来支持。目前先不提供,我们在自己的代码简单读取输出测试即可。
14.测试
回到正题,程序改完后,我们再来测试一遍。
结果:
好,现在哈希值已经正常了,但ProcessBlock还是没成功,看报错提示,大概是ConnectBlock那里失败的。
但是注意这里,跟之前有不同,他是写索引时失败的。
那么:
// 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");根据这句,已经定了区块数据到磁盘,我们可以用blockview查看一下blk0001.dat文件,如下:
已经有了第二个区块,这是我意料之中,但数据为什么不对呢?好像是个空block一样,哈希值不正常,tx也一个没有,没有coinbase。
之前控制台都已经显示挖矿后的区块数据了,并且都是正常的,为什么写入的时候出错了呢?
传递有问题?如果是这样,那后面的索引添加不进去也就正常了。因为这样的区块根本通过不了验证。
我们来具体看一下。
15.添加索引报错
我们在WriteToDisk函数,前面加上这一句,如果值正常,那么说明写入出了问题,或者是我们的blockview出了问题,第二个区块没正常读出,毕竟是用ai写的,各种情况下也没测试过。
bool WriteToDisk(bool fWriteTransactions, unsigned int& nFileRet, unsigned int& nBlockPosRet) { cout << hashPrevBlock.ToString()<< endl;再次运行,在这里能正常输出哈希值,说明是写入时出了问题,或者较小的可能blockview读取错误。应该是写入错误,如果blockview读取错误,后面添加索引应该不会出错。
因为添加索引时,会读取区块数据验证,不通过,说明数据本身错误。
16.找到问题
这个的区块读取出来是空的,是我之前忘掉了一个逻辑,我忘记了ConnectBlock失败是会清空写入Disk的区块数据的,如下代码:
if (!ConnectBlock(txdb, pindexNew) || !txdb.WriteHashBestChain(hash)); { txdb.TxnAbort(); pindexNew->EraseBlockFromDisk();看到EraseBlockFromDisk函数了吗,执行了这个操作:
bool EraseBlockFromDisk() { // Open history file CAutoFile fileout = OpenBlockFile(nFile, nBlockPos, "rb+"); if (!fileout) return false; // Overwrite with empty null block CBlock block; block.SetNull(); fileout << block; return true; }这里会把区块都设置为null,然后覆盖掉失败的block,所以你通过blockview看到的区块,数据都为0。
在这里,如果blk0001.dat有无用数据不用担心,因为程序读取是根据索引文件来的,如果索引没记录这段数据,程序就不会读取到那段数据。而这些数据进不了索引,因为添加索引是要多重验证的。
17.最终问题
好,那现在的问题就是添加区块索引失败,无法通过验证。在ConnectBlock函数里。
为了省事,我直接在ConnectBlock设置断点,然后一步一步执行,发现没有触发返回false的条件,最后返回的是true;
但是,回滚操作还是执行了,我就用代码进行输出测试一下:
// Adding to current best branch bool r1, r2; r1 = !ConnectBlock(txdb, pindexNew); r2 = txdb.WriteHashBestChain(hash); cout << r1 << "||" << r2 << endl; cout << "break" << endl; return true; /* if (!ConnectBlock(txdb, pindexNew) || txdb.WriteHashBestChain(hash));*/ { txdb.TxnAbort(); pindexNew->EraseBlockFromDisk(); mapBlockIndex.erase(pindexNew->GetBlockHash()); delete pindexNew; return error("AddToBlockIndex() : ConnectBlock failed"); }发现输出这样:
0||1 break说明是txdb.WriteHashBestChain(hash);出的问题,但是按逻辑,写入成功返回true没毛病啊。
那这样又必须会执行回滚操作,然后我翻了源码,这句前面是有个!的,不知道为什么我复制的时候不见了,可能在动代码的时候不心删掉了吧。
给他加上,如下:
else if (hashPrevBlock == hashBestChain) { // Adding to current best branch return true; if (!ConnectBlock(txdb, pindexNew) || !txdb.WriteHashBestChain(hash)); { txdb.TxnAbort(); pindexNew->EraseBlockFromDisk(); mapBlockIndex.erase(pindexNew->GetBlockHash()); delete pindexNew; return error("AddToBlockIndex() : ConnectBlock failed"); }18.解决问题
好,这次运行正常了:
区块已被ACCEPTED,添加索引成功。
由于调试后,区块数据已被干扰。
我们重新来过,删掉,blk0001.dat和blkindex.dat文件。
然后调用如下代码,不要进行BitcoinMiner,重新创建文件,写入创世块:
void Init2() { LoadBlockIndex(); LoadWallet(); } int main() { Init2(); /* fGenerateBitcoins = 1; BitcoinMiner();*/ }然后我们再将后两句的注释取消,创建第二个区块:
ok,成功添加第二个区块,我们用blockview查看一下,对一下数据:
没有问题,好现在我们有了两个区块了。
18.索引读取测试
好,现在我们关掉程序,注释掉挖矿语句,然后加载索引。
看一下第二个区块是否会正常加载。
测试索引函数:
void PrintBlockIndex() { printf("Total Blocks: %lu\n", (unsigned long)mapBlockIndex.size()); for (map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.begin(); mi != mapBlockIndex.end(); ++mi) { CBlockIndex* pindex = mi->second; if (pindex == NULL) continue; printf("--------------------------------\n"); printf("Block Hash: %s\n", mi->first.ToString().c_str()); printf("Height: %d\n", pindex->nHeight); printf("nTime: %u\n", pindex->nTime); printf("nVersion: %d\n", pindex->nVersion); printf("nBits: %u\n", pindex->nBits); printf("nNonce: %u\n", pindex->nNonce); printf("Merkle Root: %s\n", pindex->hashMerkleRoot.ToString().c_str()); printf("nFile: %u\n", pindex->nFile); printf("nBlockPos: %u\n", pindex->nBlockPos); } }然后main调用:
void Init2() { LoadBlockIndex(); LoadWallet(); } int main() { Init2(); PrintBlockIndex(); }输出结果:
sending: version (46 bytes) dbenv.open strAppDir=c:\test\btc LoadBlockIndex(): hashBestChain=00000b60c69437 height=0 fGenerateBitcoins = 0 nTransactionFee = 0 Total Blocks: 1 -------------------------------- Block Hash: 00000b60c6943717840ec4df7c4fc9386d9c81babf3d293761e7c31a6ae04e6b Height: 0 nTime: 1792561819 nVersion: 1 nBits: 520097791 nNonce: 272822 Merkle Root: a564aab2c6e83f5f36b08a0a9a7a30c1fa76f662f806a4b5c431b77497e5a31d nFile: 1 nBlockPos: 8 C:\work\btc\ConsoleApplication2\x64\Debug\ConsoleApplication2.exe (进程 11780)已退出,代码为 0 (0x0)。 要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。 按任意键关闭此窗口. . .但是很奇怪,并没有输出第二个区块,是读取错误,还是索引数据写入失败?
篇幅所限,下一章中继续解决该问题。