资讯动态

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

发布时间:2026/10/27 10:29:51 来源:尧图企业网站定制
好我们接着上章解决BitcoinMiner函数的问题。1.cs_mapTransactionscs_mapTransactions未定义我们在main.cpp最上面定义一下即可。CCriticalSection cs_mapTransactions;剩下的问题碰到了再解决我们先来解释一下代码。2.coinbase挖矿节点就得创建一个区块而区块的coinbase交易默认都是这个区块的第一笔tx交易。所以我们一开始就得创建一个conbase交易如下// // Create coinbase tx // CTransaction txNew; txNew.vin.resize(1); txNew.vin[0].prevout.SetNull(); txNew.vin[0].scriptSig nBits bnExtraNonce; txNew.vout.resize(1); txNew.vout[0].scriptPubKey key.GetPubKey() OP_CHECKSIG;好可以看到coinbase交易vin.prevout没有引用调用SetNull设置。然后vout里的nValue在这里没有被赋予因为矿工奖励要根据后面具体来计算。矿工地址就是我新创建的账号地址key.GetPubKey();3.构建新块然后是构建新块并把之前的coinbase加入到新块里为后面的挖矿做准备// // Create new block // auto_ptrCBlock pblock(new CBlock()); if (!pblock.get()) return false; // Add our coinbase tx as first transaction pblock-vtx.push_back(txNew);4.添加普通交易接下来的代码是从内存交易池中添加正常的交易到区块中然后组装成一个完整的区块进行挖矿。可以看到是从mapTransactions遍历查找添加的。它保存的是节点当前内存中待处理的交易。这个是其它线程添加的比如网络接收到带tx消息时。获取到一笔交易。然后添加。好由于我们现在只创建带coinbase的区块暂时不用其它交易。所以这部分要注释掉但我们还得大概看一下代码看看哪里有没有不能注释的代码。好我们先来看遍历mapTransactions的语句挑选交易for (mapuint256, CTransaction::iterator mi mapTransactions.begin(); mi ! mapTransactions.end(); mi, n)5.vfAlreadyAdded最后一个每一次循环n加1。对应着vectorchar vfAlreadyAdded(mapTransactions.size());初始值全为假然后如果交易添加成功则给相应那个元素赋值为true。如下vfAlreadyAdded[n] true;这个是通过序号来标记这笔交易是否已经添加而不是直接标记这笔交易的哈希值的方式。比如可以访问vfAlreadyAdded[5]如果为true,那么意为mapTransactions里的第5笔交易已经被添加进去了。为什么要这样标记呢因为一次遍历有可能有的交易没添加进去是因为它的引用找不到不是在数据库里而是引用的内存池的另一笔交易中也就是它的引用不是上一个区块而是同一个区块。所以比如第一遍要先把它的引用的交易添加进去。然后再遍历。直到这一次遍历再也无法添加一笔交易进去时说明该添加的已经添加了就结束了。所以你会在这个循环开头看到fFoundSomething false;然后添加交易成功就会看到这个设置fFoundSomething true;当然结束添加循环还有一个限制就是区块不能超过特定的大小while (fFoundSomething nBlockSize MAX_SIZE/2)MAX_SIZE/2具体多少数值可自行查看源码定义这里是以字节为单位的为什么要除以2呢因为这里的大小只是交易部分区块还有其它数据所以预留了一半。6.mapTestPool前面说过如果这笔交易依赖的是同一个区块的交易需要多次遍历先添加被依赖的交易。如果碰到这种情况那么具体建立依赖的时候是怎么做的呢通过mapTestPool方法把先添加的交易再放进一个临时的mapTestPool中。我们来看核心代码mapuint256, CTxIndex mapTestPoolTmp(mapTestPool); if (!tx.ConnectInputs(txdb, mapTestPoolTmp, CDiskTxPos(1,1,1), 0, nFees, false, true, nMinFee)) continue; swap(mapTestPool, mapTestPoolTmp);我们先来看一下第一句将mapTestPool复制到mapTestPoolTmp然后经过ConnectInputs函数后调用swap将更改后的mapTestPoolTmp,赋给mapTestPool。这里为什么还要建立一个临时的mapTestPoolTmp而不是直接修改mapTestPool呢这是怕数据污染如果ConnectInput里修改了mapTestPool但后面的代码执行又失败了。此时mapTestPool的数据就混乱了。所以通过临时的方法隔离如果执行失败我直接不swap就不会修改mapTestPool里的值了。好了具体是怎么维护这个mapTestPool引用交易池的我们需要进到ConnectInputs函数里查看。7.ConnectInputs其实这个函数在第二十章有讲过只过那里是在AddToBlockIndex里被调用的比如接收到一个新区块时需要ConnectInputs函数来验证一笔tx它的引用是否合法等。我这里是BitcoinMiner函数调用组合新区块当然也要验证我添加的tx它的引用是否合法你的签名对不对是否已经花费了不能重复花费等。8.fFound这里就不完整的重复讲这个函数了我只补充一些东西比如下面的一些判断初始值:bool fFound true;然后下面的判断:if (!fFound (fBlock || fMiner)) return fMiner ? false : error(ConnectInputs() : %s prev tx %s index entry not found, GetHash().ToString().substr(0,6).c_str(), prevout.hash.ToString().substr(0,6).c_str());9.fMiner我们先来看第一种情况如果是在fMiner模式下fFound为假说明了什么说明这笔交易(vin),没找到引用的交易。也就是在mapTestPool和数据库里都没有。那么这笔引用的交易哪里去了呢(正常情况下),那么就是在同一个区块里还在mapTransactions里面所有找不到。那么此时就会判断eturn fMiner ? false : error()如果是挖矿模式那么就返回fasle让上一层继续遍历mapTransactions将被引用的交易优先添加放在前头。此机制控制。好如果不是挖矿模式那则直接程序报错。10.fBlock好此时还有一种情况你接收了别人的区块呢就是fBlock模式那此时这个区块的交易索引也没写入数据库啊而且mapTestPool也是同样没有啊你怎么能直接给人报错啊。这是合理的啊。为什么不加个判断呢其实是如果已经构造好的块是不会有这种问题的因为fBlock模式里面的交易已经排好序了。还记得fMiner里的那个机制吗所以如果一个A区块里面的tx2引用了tx1那么tx1最后就一定会排在tx2的前面那么当ConnectInputs处理这个区块时就会先处理tx1然后你将会在这个函数的后面看到这样一句if (fBlock) { // Add transaction to disk index if (!txdb.AddTxIndex(*this, posThisTx, nHeight)) return error(ConnectInputs() : AddTxPos failed); } else if (fMiner) { // Add transaction to test pool mapTestPool[GetHash()] CTxIndex(CDiskTxPos(1,1,1), vout.size()); }处理一笔tx就会将tx索引写入数据库所以你不用担心fBlock模式下在数据库里找不到引用。11.iFound逻辑还是iFound,我们回到开始:if (!fFound (fBlock || fMiner)) return fMiner ? false : error(ConnectInputs() : %s prev tx %s index entry not found, GetHash().ToString().substr(0,6).c_str(), prevout.hash.ToString().substr(0,6).c_str()); // Read txPrev CTransaction txPrev; if (!fFound || txindex.pos CDiskTxPos(1,1,1)) {我们来看这部分代码好像有点问题就是如果找不到前面的交易,iFound则必然为假那么第一个判断则一定成立。也就说程序到这里就会结束如果程序不结束为iFound则一定为true。那么为什么后面还要判断一下fFound呢也就是这部分代码// Read txPrev CTransaction txPrev; if (!fFound || txindex.pos CDiskTxPos(1,1,1)) { // Get prev tx from single transactions in memory CRITICAL_BLOCK(cs_mapTransactions) { if (!mapTransactions.count(prevout.hash)) return error(ConnectInputs() : %s mapTransactions prev not found %s, GetHash().ToString().substr(0,6).c_str(), prevout.hash.ToString().substr(0,6).c_str()); txPrev mapTransactions[prevout.hash]; } if (!fFound) txindex.vSpent.resize(txPrev.vout.size()); } else { // Get prev tx from disk if (!txPrev.ReadFromDisk(txindex.pos)) return error(ConnectInputs() : %s ReadFromDisk prev tx %s failed, GetHash().ToString().substr(0,6).c_str(), prevout.hash.ToString().substr(0,6).c_str()); }我们先看这部分代码主要是干啥就是读取到引用的交易索引后再读取这笔引用的交易的实体根据索引。但是前面说了这笔引用的交易有可能没写进数据库里所以你要判断一下在Disk里读取还是在mapTransactions里读取。判断依据就是根据该索引的磁盘位置记录即txindex.pos。这里增加iFound判断主要就是为了这句if (!fFound) txindex.vSpent.resize(txPrev.vout.size());但不是说了fFound一定为true吗12.AcceptTransaction其实都是误导还有一个场景会调用ConnectInputs那就是节点接收到一笔新交易从其他节点或者用户创建当把它加入到交易池需要验证一下这笔交易在main.cpp里的AcceptTransaction函数中有如下代码if (fCheckInputs !ConnectInputs(txdb, mapUnused, CDiskTxPos(1,1,1), 0, nFees, false, false)) { if (pfMissingInputs) *pfMissingInputs true; return error(AcceptTransaction() : ConnectInputs failed %s, hash.ToString().substr(0,6).c_str()); } // Store transaction in memory CRITICAL_BLOCK(cs_mapTransactions) { if (ptxOld) { printf(mapTransaction.erase(%s) replacing with new version\n, ptxOld-GetHash().ToString().c_str()); mapTransactions.erase(ptxOld-GetHash()); } AddToMemoryPool(); }看到没有ConnectInputs函数的第六,七个参数传入都为false也就是fBlock和fMiner都为fasle.此时我们再跳到ConnectInputs中假设这笔交易引用的交易在数据库里都找不到。也就是fFound为false了。但是这句判断if (!fFound (fBlock || fMiner)) return fMiner ? false : error(ConnectInputs() : %s prev tx %s index entry not found, GetHash().ToString().substr(0,6).c_str(), prevout.hash.ToString().substr(0,6).c_str());由于fBlock和fMiner都为false所以里面的代码不会执行。这就存在了一个情况iFound为false也能跳过这个判断。所以接下来的代码// Read txPrev CTransaction txPrev; if (!fFound || txindex.pos CDiskTxPos(1,1,1)) { // Get prev tx from single transactions in memory CRITICAL_BLOCK(cs_mapTransactions) { if (!mapTransactions.count(prevout.hash)) return error(ConnectInputs() : %s mapTransactions prev not found %s, GetHash().ToString().substr(0,6).c_str(), prevout.hash.ToString().substr(0,6).c_str()); txPrev mapTransactions[prevout.hash]; } if (!fFound) txindex.vSpent.resize(txPrev.vout.size()); } else { // Get prev tx from disk if (!txPrev.ReadFromDisk(txindex.pos)) return error(ConnectInputs() : %s ReadFromDisk prev tx %s failed, GetHash().ToString().substr(0,6).c_str(), prevout.hash.ToString().substr(0,6).c_str()); }存在iFound为false的情况也就是接收到一笔交易但是它的引用在数据库里读不到。然后就在mapTransactions里查找当前节点的内存交易池。如果还找不到就直接报错。也就是说如果节点接收到一笔交易该交易引用了分叉块上的交易而那个分叉块节点又没有保存那么此交易是会被拒绝的。在这里我们就假设正好在mapTransactions里找到了吧。那么此时是什么情况就是你的交易索引没读取到但先读取到了实体所以我们可以通过实体来设置索引就会有如下的写法if (!fFound) txindex.vSpent.resize(txPrev.vout.size());设置一下该实体对应的索引所有的vout未花费。注意这里只是临时构建实际你并不知道是不是都未花费比如其实另一笔交易也引用了其下一个vout只是你不知道。但可以肯定的是在本节点中都没有写进数据库里。所以在后面的判断中比如写进数据库里才会确定也不用怕双花攻击最终写入数据库里有个先来后到。具体细节就不看了。只是说明一下不用担心。而txindex的pos数据因为还未写进数据库所以这里不设置我们来看一下txindex数据结构就是这样标记tx硬盘位置和记录其下vout有没有花费class CTxIndex { public: CDiskTxPos pos; vectorCDiskTxPos vSpent; CTxIndex() { SetNull(); }好如果我们是fMiner模式过了这个判断if (!fFound (fBlock || fMiner))我们就当fFound不存在就行按正常的逻辑理解后面的代码。即只有两个判断即引用的交易索引不在数据库里又怎么实例化tx。反之又怎么样后续的代码已经给出。13.要求判断然后后面就是一些格式数值以及其它的一些规则判断要求引用交易必须规范。当然脚本验证也是在这里验证你是否对引用的交易拥有所有权以及那笔交易有没有花费过。都通过了之后。那么就是我的引用通过了那么此时引用的那笔交易索引是不是得修改一下了。13.修改引用交易索引的状态比如有的vout未花费状态变成已花费// Mark outpoints as spent txindex.vSpent[prevout.n] posThisTx; // Write back if (fBlock) txdb.UpdateTxIndex(prevout.hash, txindex); else if (fMiner) mapTestPool[prevout.hash] txindex;注意这里的标记已花费不是true和false标记而是记录花费它的tx磁盘位置。也就是说当我们访问某个vout的vSpent时发现它有对应的位置就是已经花费了。当然这是写入数据库的事目前我们这笔交易还未写到数据库里所以在挖矿函数赋值是为CDiskTxPos(1,1,1)if (!tx.ConnectInputs(txdb, mapTestPoolTmp, CDiskTxPos(1, 1, 1), 0, nFees, false, true, nMinFee))修改完了之后要干什么呢当然是把这个修改更新数据库中如果数据库里没有就更新mapTestPool里的索引。14.手续费后面一段是计算手续费然后是将此交易索引根据情况(注意上面是写引用交易索引这次是写本交易索引写进数据库里还是mapTestPool中。好了后面那个写本索引到数据库里之前已经提到过了。也就是到此时我们由于是创世块所以的代码现在都用不到。然后我们再来看看手续费部分后面是否也可以注释还是得修改。当然不是说我们理解注释部分没用如果后面的章节中如果生成正常交易的区块。我们就可以不用重头来理解注释部分的代码因为已经提前学习了。在后面大有用处。接下来我们来看手续费系统是什么样的// Tally transaction fees int64 nTxFee nValueIn - GetValueOut(); if (nTxFee 0) return error(ConnectInputs() : %s nTxFee 0, GetHash().ToString().substr(0,6).c_str()); if (nTxFee nMinFee) return false; nFees nTxFee;nTxFee计算的就是这笔tx输入减去输出剩下的就是手续费但这里有个问题如果我输入引用1btc然后输出转给别人0.5btc那矿工不是白嫖了我0.5个btc吗别急其实这里是包括找零的等于说交易构建好后已经给自己找零了。这个手续还是你自己决定给多少比如给自己找零多少是由你这边决定的。15.nTxFee好我们来看第一个手续费判断if (nTxFee 0)如果仅有这个判断还好说说明什么说明nTxFee可以为0。也就是说输入和输出完全相等也是合法的不用给手续费。16.nMinFee但是后面还有个判断if (nTxFee nMinFee) return false;也就是说每笔交易的手续续有个最低要求你不能低于nMinFee。你必须给矿工一点。那这个nMinFee是怎么算的呢我们得看它的调用处比如说是挖矿模式int64 nMinFee tx.GetMinFee(pblock-vtx.size() 100); mapuint256, CTxIndex mapTestPoolTmp(mapTestPool); if (!tx.ConnectInputs(txdb, mapTestPoolTmp, CDiskTxPos(1, 1, 1), 0, nFees, false, true, nMinFee))是函数传进来的nMinFee。在上面有计算规则// Transaction fee requirements, mainly only needed for flood control // Under 10K (about 80 inputs) is free for first 100 transactions // Base rate is 0.01 per KB int64 nMinFee tx.GetMinFee(pblock-vtx.size() 100);GetMinFeeint64 GetMinFee(bool fDiscountfalse) const { unsigned int nBytes ::GetSerializeSize(*this, SER_NETWORK); if (fDiscount nBytes 10000) return 0; return (1 (int64)nBytes / 1000) * CENT; }通过函数代码我们可以知道这个区块的前100条tx免费前提是这条tx不大于10kb,否则按大小计费具体费用是(100条以后同理):return (1 (int64)nBytes / 1000) * CENT;这里有个/10001是不足1000按1算再乘以CENT,说明是按kb收费,也就是说每kb CENT(btc)CENT我们来看CENT的值就知道是多少了static const int64 CENT 1000000;注意这里的单位是聪1000000聪0.01btc也就是每kb 0.01btc。17.nFeesnFees nTxFee;知道了这笔tx具体手续后就把它累加到nFees里面。17.GetBlockValue然后挖矿函数将所有交易添加完成后就会计算给矿工多少费用包括本身挖矿奖励:pblock-vtx[0].vout[0].nValue pblock-GetBlockValue(nFees);默认区块第一笔tx就是coinbase交易。int64 CBlock::GetBlockValue(int64 nFees) const { int64 nSubsidy 50 * COIN; // Subsidy is cut in half every 4 years nSubsidy (nBestHeight / 210000); return nSubsidy nFees; }就是将矿工本身奖励和手续费相加一下当然这个挖矿奖励还包含着减半算法就是对应着每4年减半。18.总结费用好,由于我现在只是coinbase交易这个区块也只包含coinbase交易手续nFee就默认为0不需要手续费而且如果有那也是从自己那里扣然后又转给自己。也不符合逻辑。coinbase索引BitcoinMiner里的代码不用注释因为如果我第二个区块只有coinbase交易的话那么等于mapTransactions里就没有交易是空的所以抓取交易这段代码就不会被执行相当于被注释了。那这里有个问题如果那段代码不会被执行那么ConnectInputs函数就不会被执行相应的coinbase tx索引不会被构建也就是以后无法被引用了。因为数据库索引里找不到这笔tx。别担心在挖矿里coinbase索引的写入走的不是这个ConnectInputs而是另一个地方调用的ConnectInputs。我们来看顺序在挖矿成功后就会调用:ProcessBlock-AcceptBlock-AddToBlockIndex-ConnectBlock-ConnectInputs在这里调用了ConnectInputs处理我们知道在ConnectInputs函数里如果是coinbase交易则不会验证引用的交易而是直接写入这笔tx index到数据库里。这样coinbase就在数据库库有了对应的索引。19.创世块无法花费创世块则直接不走ConnectInputs函数所以创世块的那个tx自然没有建立对应的tx索引(只建了block index那么如果一个交易引用在验证的时候他从数据库块查不到这个tx索引自然也就无法通过验证所以这就是创世块的那笔钱用不了的原因。20.解决方案好关于BitcoinMiner代码注释我们选择不注释跟原版本一样。那么我们就需要修复代码解决定义问题。下一章中开始。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑