1.CBlock未定义问题在这之前先来解决一个问题就是之前我不是将CBlock类代码移到了main.h中吗。为了遵照源码的写法然后将CBlockIndex和CDiskBlockIndex都写进了main.h中。但报大量错误使用了未定义的class CBlock。因为在CBlockIndex和CDiskBlockIndex大量使用了CBlock,就报这里的错误。我当时就奇怪了我声明了啊。最上面有class CBlock。然后我把这三个类放到main.cpp就不报错奇怪。最终跟什么main.h和main.cpp无关。始于我的一个误解。因为C这门语言是我很久以前学的有一些东西都忘记了。只是临时复习了一段时间。没有去了解这个类声明我以为类声明只要class CBlock这样就行了这种声明只能定义一个指针是不报错的如果你要构建对象或者调用类里的函数是会报错你需要声明整个类的结构变量和函数才行。所以我在main.h中是将CBlockIndex和CDiskBlockIndex定义在CBlock前面的。而在main.cpp因为我没有那个错误的class CBlock的声明我下意识的就将CBlock定义在他们的前面。所以导致这种假象然后我又去看了源码中的定义确实是CBlock定义在这两个类的前面。问题出在我打乱顺序所以将main.h中的这三个类的声明改好顺序就行就能解决这个问题。然后这里我们要将CTransaction及其下涉及所有交易结构的类都搬到main.h中(注意顺序按main.cpp中的顺序来。否则会有声明问题因为这些我们都没有声明。然后将下面这一长串的声明复制到main.h最上面class COutPoint; class CInPoint; class CDiskTxPos; class CCoinBase; class CTxIn; class CTxOut; class CTransaction; class CBlock; class CBlockIndex; class CWalletTx; class CKeyItem;这种声明是没问题的就算没有具体的定义只要没有调用也是可以的所以我们将CkeyItem这些还没用到的声明一下也是可以的为了省事所以都复制过来了2.CTxDB好现在AddToBlockIndex函数就只剩下的CTxDB问题了我们转到db.cpp,db.h恢复CTxDB类。我们先将db.h中的CTxDB声明注释取消恢复代码。然后转到db.cpp中取消所有CTxDB相关的代码注释。2.CTxIndex txindex先来解决第一个报错不允许使用不完整的类型txindex是因为这个类型CTxIndex只有声明没有定义。相关代码如下bool CTxDB::ReadTxIndex(uint256 hash, CTxIndex txindex) { assert(!fClient); txindex.SetNull(); return Read(make_pair(string(tx), hash), txindex); }这个ReadTxIndex是在读什么呢他根据哈希值可以读取这笔交易数据在文件中的编号所属块的偏移以及具体那笔交易数据的偏移。注意这个哈希值不是CBlock的哈希值而是CTransaction的哈希值即一笔交易的哈希值txid。这就是从DB数据库里读的也就是说每笔交易数据的索引也被存到数据库里不仅仅是之前的区块索引。那么这必然又有着另一套相似的但不同名索引系统。一个是block一个是tx.所以这里的CTxIndex其实就对应着CBlockIndex我们看它们的取名就知道如果是tx相关的索引和数据读取就带有Tx标志如果是Block就带有Block.那么们来看看CTxIndex的具体结构// CTxIndex 类用于存储一笔交易的磁盘位置索引信息 // 以及该交易输出outputs被花费的情况 // // 这是早期比特币交易数据库TxDB中非常重要的一个结构。 // 它的主要作用是通过交易哈希txid快速定位该交易在磁盘上的位置 // 并记录它的每个输出是否已经被后续交易花费。 // // 注意vSpent 虽然存储了花费位置但早期主要用作“是否已花费”的标志 // 实际位置信息主要用于调试和开发。 // class CTxIndex { public: // 该交易在磁盘上的具体位置哪个 blk 文件、哪个区块、交易在区块中的偏移量 CDiskTxPos pos; // 记录该交易每个输出output被花费的情况 // vSpent[i] 表示第 i 个输出被哪笔交易的哪个输入所花费 // 如果 vSpent[i] 是空IsNull()则说明该输出尚未被花费 // Satoshi 时代主要把 vSpent 当作“已花费标志”使用位置信息更多用于调试 vectorCDiskTxPos vSpent; // 默认构造函数将对象初始化为空状态 CTxIndex() { SetNull(); } // 带参数的构造函数 // posIn交易在磁盘上的位置 // nOutputs该交易的输出数量用于初始化 vSpent 的大小 CTxIndex(const CDiskTxPos posIn, unsigned int nOutputs) { pos posIn; vSpent.resize(nOutputs); // 根据输出数量分配空间 } // 序列化 / 反序列化支持用于写入和读取 Berkeley DB 数据库 // IMPLEMENT_SERIALIZE 是早期比特币自定义的宏用于简化序列化代码 IMPLEMENT_SERIALIZE ( if (!(nType SER_GETHASH)) READWRITE(nVersion); // 版本号早期版本中通常不使用 READWRITE(pos); // 写入/读取交易位置 READWRITE(vSpent); // 写入/读取每个输出的花费记录 ) // 将对象重置为空状态 void SetNull() { pos.SetNull(); // 清空位置信息 vSpent.clear(); // 清空花费记录 } // 判断当前 CTxIndex 是否为空即数据库中没有找到该交易的索引 bool IsNull() { return pos.IsNull(); } // 重载 运算符用于比较两个 CTxIndex 是否完全相同 friend bool operator(const CTxIndex a, const CTxIndex b) { if (a.pos ! b.pos || a.vSpent.size() ! b.vSpent.size()) return false; // 逐个比较 vSpent 中的每个元素 for (int i 0; i a.vSpent.size(); i) if (a.vSpent[i] ! b.vSpent[i]) return false; return true; } // 重载 ! 运算符直接调用 的反结果 friend bool operator!(const CTxIndex a, const CTxIndex b) { return !(a b); } };3.CDiskTxPos这个类下面还有一个关键的类型CDiskTxPos,顾名思义Disk和tx,pos就表明磁盘上tx的位置。定义如下// CDiskTxPos 类表示一笔交易或交易输出在磁盘文件中的物理位置 // // 这是早期比特币存储系统中非常重要的一个定位结构。 // 比特币把所有区块数据顺序追加写入 blk00000.dat、blk00001.dat 等文件中 // CDiskTxPos 就是用来精确记录“某笔交易到底写在哪个文件、哪个位置”的。 // class CDiskTxPos { public: // 区块数据文件编号从 0 开始 // 例如0 表示 blk00000.dat1 表示 blk00001.dat以此类推 unsigned int nFile; // 在该 blk 文件中区块头CBlock的起始位置字节偏移量 // 即该交易所属的区块从文件的哪个字节开始 unsigned int nBlockPos; // 在该区块内部交易数据CTransaction的起始位置字节偏移量 // 即交易的具体位置 区块起始位置 nTxPos unsigned int nTxPos; // 默认构造函数初始化为空位置 CDiskTxPos() { SetNull(); } // 带参数构造函数直接设置文件编号、区块位置和交易位置 CDiskTxPos(unsigned int nFileIn, unsigned int nBlockPosIn, unsigned int nTxPosIn) { nFile nFileIn; nBlockPos nBlockPosIn; nTxPos nTxPosIn; } // 序列化支持用于写入和读取 Berkeley DB 数据库 // FLATDATA(*this) 表示把整个结构体当作字节数组直接读写效率较高 IMPLEMENT_SERIALIZE( READWRITE(FLATDATA(*this)); ) // 将位置信息重置为空状态无效位置 // nFile -1 是约定俗成的“空位置”标记 void SetNull() { nFile -1; nBlockPos 0; nTxPos 0; } // 判断当前位置是否为空无效 // 当 nFile -1 时表示该位置未设置或不存在 bool IsNull() const { return (nFile -1); } // 重载 运算符判断两个位置是否完全相同 friend bool operator(const CDiskTxPos a, const CDiskTxPos b) { return (a.nFile b.nFile a.nBlockPos b.nBlockPos a.nTxPos b.nTxPos); } // 重载 ! 运算符 friend bool operator!(const CDiskTxPos a, const CDiskTxPos b) { return !(a b); } // 将位置信息转换为字符串便于打印和调试 string ToString() const { if (IsNull()) return strprintf(null); else return strprintf((nFile%d, nBlockPos%d, nTxPos%d), nFile, nBlockPos, nTxPos); } // 打印位置信息到控制台调试用 void print() const { printf(%s\n, ToString().c_str()); } };好我们把这两个类放进main.h中注意顺序然后让CTxDB能正常读取tx索引到数据库中。让这些索引数据有一个承载的对象。4.CTransactionif (nItemHeight nMinHeight) { vtx.resize(vtx.size() 1); if (!vtx.back().ReadFromDisk(pos)) return false; }4.1ReadFromDisk这里这里会报错就是ReadFromDisk函数在CTransaction里我们并没有定义。因为我们现在用的是简写的CTransaction所以很多东西没有。这个ReadFromDisk就跟CBlock里的函数一样根据位置从磁盘读取一个tx实例化自身。我们将这个函数添加进CTransaction中bool ReadFromDisk(CDiskTxPos pos, FILE** pfileRet NULL) { CAutoFile filein OpenBlockFile(pos.nFile, 0, pfileRet ? rb : rb); if (!filein) return error(CTransaction::ReadFromDisk() : OpenBlockFile failed); // Read transaction if (fseek(filein, pos.nTxPos, SEEK_SET) ! 0) return error(CTransaction::ReadFromDisk() : fseek failed); filein *this; // Return file pointer if (pfileRet) { if (fseek(filein, pos.nTxPos, SEEK_SET) ! 0) return error(CTransaction::ReadFromDisk() : second fseek failed); *pfileRet filein.release(); } return true; }这里可以看到读取交易数据只指定了起始位置并没结束位置他是怎么知道要读多少的呢。关键在这句filein*this是会依据对象来读取多少字节的。也就是内部有指明大小针对对象的特殊协议。txvinvout都属于这个分类。读写这些对象时不是直接读写而是遵循一套协议先读写大小后根据大小读写相应数量的数据。4.2SetNull然后这里报错bool CTxDB::ReadDiskTx(uint256 hash, CTransaction tx, CTxIndex txindex) { assert(!fClient); tx.SetNull(); if (!ReadTxIndex(hash, txindex)) return false; return (tx.ReadFromDisk(txindex.pos)); }tx.SetNull()CTransaction没有这个SetNull函数。将函数代码添加进去void SetNull() { nVersion 1; vin.clear(); vout.clear(); nLockTime 0; }然后给CTransaction类成员定义如下class CTransaction { public: int nVersion; vectorCTxIn vin; vectorCTxOut vout; int nLockTime;5.LoadBlockIndex好前面都没问题了现在就剩下解决CTxDB的LoadBlockIndex函数的问题了。这跟我们之前解决CWalletDB下的LoadWallet是同样的工作。1.pindexGenesisBlockCBlockIndex* pindexGenesisBlock NULL;将上面这个在main.cpp和main.h定义和声明一下。2.hashGenesisBlock然后是const uint256 hashGenesisBlock(0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f);同样定义和声明,但是这个创世哈希值后续我们可能需要更改。6.hashBestChain然后是LoadBlockIndex里的这段代码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);最优链相关操作我们暂时不要直接全部注释掉。7.fClient然后报db.obj中无法解析的外部符号fClient我在db.h看到这句extern bool fClient;只有声明但没有定义奇怪可能我之前忘记了吧。这个看源码定义在net.h中我们直接加上bool fClient false;但是加上后又报fClient重定义然后我又把net.h中的定义注释掉。然后又报未定义我仔细看一下是db.obj和main.obj报这个未定义说明db.cpp和main.cpp这两个cpp使用了fClient这个变量。然后我又去看了下头文件我发现为什么把这种非static变量放在net.h中呢如果多次包含这样不是很容易引起重定义吗然后一看嗨原来是我看错了这个是在net.cpp中定义的。我们没有net.cpp所以有些忽视了这样我们新建一个net.cpp吧添加如下代码解决即可#includeheaders.h // // Global state variables // bool fClient false;好现在程序编译能正常运行了。接下来我们准备测试读写索引功能了以便调试遇到什么问题再进行代码的添加因为被我注释的代码我感觉有些还是必须要用上的。我们一边测试一边查找。8.准备测试改好简写后的bool CBlock::AddToBlockIndex(unsigned int nFile, unsigned int nBlockPos)函数只做两件事将区块索引写进内存mapBlockIndex以及磁盘CTxDB数据库中。不涉及根据分叉判断来写也没有写交易索引只写区块索引。也没有验证以及网络相关部分。现在就是纯粹的写一个区块索引。这个函数在哪调用的呢在CBlock中的AcceptBlock函数中调用WriteToDisk写完区块数据后就会接着调用AddToBlockIndex写索引。所以我们现在把它们两个都放在AcceptBlock函数里,并删除之前的blk0001.dat文件我们需要重新测试这次增加了添加索引的功能。这次不是单独调用WriteToDisk)。我们先来看一下原本的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; }在接受块之前有很多判断。9.判断重复块mapBlockIndex.count(hash)这个不用管判断这个哈希值是否重复就是已经存在添加过了。10.创世块生成mi mapBlockIndex.end()这个是查找它的父区块哈希如果没有则不通过。很明显我们无法通过这个判断因为我们创建的区块是单独的没有给他加上父区块。那这里有个问题如果是创世区块呢如果创世区块不在这里判断也就是说在调用AcceptBlock之前默认创世区块是存在的。它是在程序启动时先调用LoadBlockIndex全局函数判断创世块是否存在不存在则创建注意这个跟CTxDB里的LoadBlockIndex是不一样的它会先调用CTxDB里的LoadBlockIndex函数加载数据库索引如果发现为空也就是没有创世块则会创建创世块是这么个逻辑。具体代码。等以后用到的时候再贴上来。我们现在需要修改AcceptBlock函数所以这个父区块判断第二个逻辑的代码我们需要临时注释一下。以便测试我们的写索引功能。11.中位时间nTime pindexPrev-GetMedianTimePast()第三个判断是接受块的时候判断这个块的nTime是否小于中位时间如果是则拒绝接受该块。我们先来说一下什么是中位时间就是GetMedianTimePast干了什么事将前11个区块的nTime,按从小到大排序然后取中间那个nTime就是第6个块的时间。然后你的块的时间不能早于这个时间。为什么这样做为了保证区块的生成时间总是线性向前的不能倒退。否则你把nTime设置为2009年那不是也可以。而因为是去中心化没有一个权威去相信你不可能去获取系统时间来对比否则被造假怎么办。所以取前11个区块的中位时间。还有一是nLockTime,规定了我这个区块在多少时间后才能被打包也就是延时交易吧。比如现在3号我要10号这个交易才可以被确认它是怎么判断对比的呢在CTransaction里IsFinal判断但我看了具体的代码只有对比区块高度判断并不是对比nTime判断。相关功能还没实现。总而言之这个中位时间相当于一个权威的时间目的是为了防止区块时间nTime被造假不能太早也不能太晚需要一个参考物。然后就可以实现其他相关功能。比如这里的这句判断又比如后来的版本中nLockTime相关你设置nTime得通过中位检查然后才会对比nLockTime和nTime,这样即你的nTime造假有一定的误差那几乎也没有影响相差不大。所以这里的中位时间判断我们也得临时注释因为我们没有前11个区块或者前几个区块如果少于11个。它还是会取中间块的那个时间。但是我们是0个区块。而且第二个逻辑已经被我们注释了它不会获取到父区块pindexPrev所以也无从调用。所以这里的中位时间我们直接注释吧。12.GetNextWorkRequiredif (nBits ! GetNextWorkRequired(pindexPrev))这里直接注释我们同样没有pindexPrev但是还是说明一下这个nBits难度判断在前面我们判断了区块的nTime是否符合要求这里又增加了难度判断是否符合要求目的都是为防止矿工胡乱设置。换句话说这些都是对矿工提交区块的合法性检查。只大概讲一下这个是干什么的GetNextWorkRequired函数逻辑就不说明了是怎么计算难度的留待以后吧。直接注释。然后还有里也注释掉吧if (hashBestChain hash) RelayInventory(CInv(MSG_BLOCK, hash));就是这个区块是被你添加到你最优链不是分叉块也不是过时块你就会高兴的通知其他节点跟大家分享调用RelayInventory函数广播出去当然只广播哈希。是为了为止跟你有同样高兴的人。所以得验证一下哈希如果其他节点发现没有这个哈希才会请求这个块的具体数据这样做的好处是节省带宽。我们不需要广播直接注释这样注释下来其实几乎就剩两句代码了AcceptBlock只调用了WriteToDisk和AddToBlockIndex了。如下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; }11.测试代码好现在的测试很简单调用我们之前的GenerateTestBlock函数构建一个区块。然后调用块的AcceptBlock函数即可。但是这里我们还需要看一下代码创建索引文件blkindex.dat应该会碰到和wallet.dat同样的问题。class CTxDB : public CDB { public: CTxDB(const char* pszMode r, bool fTxn false) : CDB(!fClient ? blkindex.dat : NULL, pszMode, fTxn) {}它传的同样是r,也就是不支持创建文件。而我们上面的测试同样没有调用跟LoadWallet一样的LoadBlockIndex函数这是分别创建wallet.dat和blkindex.dat文件函数如果文件不存在的话。那我们这里还是遵循之前wallet.dat的解决方法在构造CTxDB对象传参数cr。(减少测试工作量不调用Load)即AddToBlockIndex函数里对应的部分改成如下代码:CTxDB txdb(cr); txdb.TxnBegin(); txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));13.测试代码好main中测试代码如下int main() { CBlock b GenerateTestBlock(); bool resb.AcceptBlock(); if (res) { cout 写区块数据和写索引目录成功! endl; auto it mapBlockIndex.begin(); // 迭代器指向第一个元素 uint256 firstHash it-first; cout 索引哈希值: firstHash.ToString() endl; CBlockIndex* pIndex it-second; cout 索引区块中的时间:(对比是否一致) pIndex-nTimeendl; } return 0; }这个GenerateTestBlock函数代码在前面的章节有给出这里就不贴代码就是填充一个块的数据。然后区块数据部分就不输出了因为WriteToDisk函数之前已测试过。这里只输出一下索引相关的数据而且是读内存中的不是数据库里的。数据库将会在读的部分给出这里仅测试写的部分只是为了展示一下数据。注程序有报错解决报错过程由于太长写在了下一章。