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