ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

四十四.创建一笔交易(7)--第二个区块AddToBlockIndex

四十四.创建一笔交易(7)--第二个区块AddToBlockIndex

题外话,其实我觉得源码中,除了区块链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)。 要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。 按任意键关闭此窗口. . .

但是很奇怪,并没有输出第二个区块,是读取错误,还是索引数据写入失败?

篇幅所限,下一章中继续解决该问题。

返回列表