☰
四十二.创建一笔交易(5)--第二个区块BitcoinMiner
2026/10/27 10:29:39 网站建设 项目流程

好,我们接着上章,解决BitcoinMiner函数的问题。

1.cs_mapTransactions

cs_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_ptr<CBlock> 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 (map<uint256, CTransaction>::iterator mi = mapTransactions.begin(); mi != mapTransactions.end(); ++mi, ++n)
5.vfAlreadyAdded

最后一个,每一次循环,n加1。

对应着:

vector<char> 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中。

我们来看核心代码:

map<uint256, 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; vector<CDiskTxPos> 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); map<uint256, 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);

GetMinFee

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; }

通过函数代码我们可以知道,这个区块的前100条tx免费,前提是这条tx不大于10kb,否则按大小计费,具体费用是(100条以后同理):

return (1 + (int64)nBytes / 1000) * CENT;

这里有个/1000,+1是不足1000按1算,再乘以CENT,说明是按kb收费,也就是说每kb CENT(btc)

CENT

我们来看CENT的值就知道是多少了:

static const int64 CENT = 1000000;

注意这里的单位是聪,1000000聪=0.01btc,也就是每kb 0.01btc。

17.nFees

nFees += 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代码注释,我们选择不注释,跟原版本一样。那么我们就需要修复代码,解决定义问题。下一章中开始。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询