我们来接上一章的ConnectBlock函数,在其下调用了这个函数。
1.ConnectInputs
这里有一句关键的代码,每遍历一次,调用了这个:
cpp
if (!tx.ConnectInputs(txdb, mapUnused, posThisTx, pindex->nHeight, nFees, true, false))
return false;
也就是调用了tx(CTransaction)下的ConnectInputs函数,这就是写交易索引了的具体地方了。
注意,写交易索引就代表了交易的合法性,所以这种函数也会有各种检测,包括验证签名等。
通过了才会写进去。
cpp
bool CTransaction::ConnectInputs(CTxDB& txdb, map<uint256, CTxIndex>& mapTestPool, CDiskTxPos posThisTx, int nHeight, int64& nFees, bool fBlock, bool fMiner, int64 nMinFee)
{
// 如果是 coinbase 交易(挖矿奖励),则不需要处理输入,直接跳过
if (!IsCoinBase())
{
int64 nValueIn = 0; // 累计本交易所有输入的总金额
// 遍历本交易的每一个输入(vin)
for (int i = 0; i < vin.size(); i++)
{
COutPoint prevout = vin[i].prevout; // 当前输入引用的前一个交易的输出点 (txid + 输出索引)
// Read txindex
CTxIndex txindex; // 用于存储前一个交易的索引信息(包含哪些输出已被花费)
bool fFound = true;
// 如果当前是在挖矿验证模式(fMiner=true),且前一个交易在本区块内已处理过
// 则优先从临时测试池 mapTestPool 中获取最新状态(支持同一区块内交易互相花费)
if (fMiner && mapTestPool.count(prevout.hash))
{
// 从当前正在验证的区块的临时池中获取 txindex
txindex = mapTestPool[prevout.hash];
}
else
{
// 否则从磁盘数据库中读取已确认的交易索引
fFound = txdb.ReadTxIndex(prevout.hash, txindex);
}
// 如果没找到前一个交易的索引,且当前是正式连接区块或挖矿验证,则报错
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))
{
// 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
{
// 从磁盘读取前一个交易的完整数据
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());
}
// 检查引用的输出索引是否合法(防止越界)
if (prevout.n >= txPrev.vout.size() || prevout.n >= txindex.vSpent.size())
return error("ConnectInputs() : %s prevout.n out of range %d %d %d", GetHash().ToString().substr(0,6).c_str(), prevout.n, txPrev.vout.size(), txindex.vSpent.size());
// 如果前一个交易是 coinbase,则检查是否已经成熟(Coinbase Maturity)
if (txPrev.IsCoinBase())
for (CBlockIndex* pindex = pindexBest; pindex && nBestHeight - pindex->nHeight < COINBASE_MATURITY-1; pindex = pindex->pprev)
if (pindex->nBlockPos == txindex.pos.nBlockPos && pindex->nFile == txindex.pos.nFile)
return error("ConnectInputs() : tried to spend coinbase at depth %d", nBestHeight - pindex->nHeight);
// 验证当前输入的签名是否正确
if (!VerifySignature(txPrev, *this, i))
return error("ConnectInputs() : %s VerifySignature failed", GetHash().ToString().substr(0,6).c_str());
// 检查这个输出是否已经被其他交易花费(双花检查)
if (!txindex.vSpent[prevout.n].IsNull())
return fMiner ? false : error("ConnectInputs() : %s prev tx already used at %s", GetHash().ToString().substr(0,6).c_str(), txindex.vSpent[prevout.n].ToString().c_str());
// Mark outpoints as spent
// 把当前输入引用的输出标记为已花费,记录花费它的交易位置
txindex.vSpent[prevout.n] = posThisTx;
// Write back
// 根据不同模式,将更新后的 txindex 写回对应位置
if (fBlock)
txdb.UpdateTxIndex(prevout.hash, txindex); // 正式区块:写入磁盘数据库
else if (fMiner)
mapTestPool[prevout.hash] = txindex; // 挖矿验证:写入临时测试池
// 累加输入金额
nValueIn += txPrev.vout[prevout.n].nValue;
}
// 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; // 累加到整个区块的手续费中
}
// 如果是正式连接区块(fBlock=true),则把本交易加入磁盘交易索引
if (fBlock)
{
if (!txdb.AddTxIndex(*this, posThisTx, nHeight))
return error("ConnectInputs() : AddTxPos failed");
}
// 如果是挖矿验证模式,则把本交易加入临时测试池(用于后续交易引用)
else if (fMiner)
{
mapTestPool[GetHash()] = CTxIndex(CDiskTxPos(1,1,1), vout.size());
}
return true;
}
2.mapTestPool
这个ConnectInputs函数,首先干的就是获取这笔tx其下的所有vin,一个vin定位到一笔vout,它就会把这笔vout所属的tx(CTransaction)的CTxIndex获取到,根据哈希值。
但是这种获取有两种情况,看下列if逻辑:
cpp
if (fMiner && mapTestPool.count(prevout.hash))
注意啊,我们这里的mapTestPool是空的,既然是空的为什么还要加上这个判断,这并不是写错了,因为调用ConnectInputs不仅仅一个地方,其他函数也会调用ConnectInputs.(fmIner=true,挖矿时)那里的mapTestPool是有效的。
我们先来说说为什么会有这个,先要了解一个概念,即一笔vin它引用的可能是同一个区块的vout。这种情况是有可能发生的。
比如差不多同一时间,A有1btc,A转给B 0.8btc,是不是还要找零给自己0.2btc。
A又把这0.2BTC,转给了C。
此时的这下一笔vin引用的vout,是不是这个同一区块的0.2btc。
那么此时。这个区块还未写进数据库,你想构建引用的这笔CTxIndex索引,你在数据库读不到,你就只能到池子中去读取。所以里会有这样一个判断,如果不是一个区块的的话,你可以看它接下来的代码:
cpp
else
{
// Read txindex from txdb
fFound = txdb.ReadTxIndex(prevout.hash, txindex);
}
直接从数据库里读取。
11.CDiskTxPos(1,1,1)
读到引用的这笔交易索引,然后干什么呢?反序列化成CTransaction,那是不是要到磁盘里读这一笔tx的数据呢?但前面说了,由于可能在于同一个区块,同样的这笔tx还未写到磁盘里,所以会有下面的判断:
cpp
if (!fFound || txindex.pos == CDiskTxPos(1,1,1))
这个标志CDiskTxPos(1,1,1),这是特定的标志,表示同一个区块,因为如果你这笔交易未写进磁盘,那肯定也没有偏移位置,所以都用1,1,1代替,表示一种特殊的CDiskTxPos。
如果磁盘里有,直接就是这一句从磁盘里读:
cpp
if (!txPrev.ReadFromDisk(txindex.pos))
如果没有,则是这个从内存池中读:
cpp
txPrev = mapTransactions[prevout.hash];
但是这里还多了一句:
cpp
if (!fFound)
txindex.vSpent.resize(txPrev.vout.size());
首先我们来分析!fFound这种情况存不存在,也就是什么时候fFound为假,引用的这笔交易mapTestPool找不到,但是根据哈希值在数据库也读取不到,但是在mapTransactions是能找到的。
那么此时就走另一条分支。这条分支txindex的vSpent是未分配空间的,所以需要分配一下。
否则后面的赋值将会报错。
3.vSpent
到了这里,我们来了解一下CTxIndex下的vSpent是干什么的,为后续的代码做铺垫。
我们先来看定义:
cpp
class CTxIndex
{
public:
CDiskTxPos pos;
vector<CDiskTxPos> vSpent;
这个vSpent记录了这笔tx已花费的vout,就是这个tx下有多少个vout,这个vSpent数组就会有多少个。每一个元素对应一笔vout,按顺序,第1个元素对应这笔tx第一个vout。
如果这个vout是已花费的,比如说第二个,那么对应的这个vSpent[2]就会被赋值指向这笔花费此vout的tx,通过CDiskTxPos定位。注意这里是指向tx,并不是具体的vin,需要知道具体的vin,是要先把整个tx读取出来,然后看其下的哪个vin引用了我这个vout,通过vin下的也就是CTxIn下的prevout.n,如果为2,则匹配成功。
好,了解了vSpent后我们来看接下来的代码。
这里的每笔vin都必须有对应的vout,如果我引用的是某笔tx下的第36个vout,但是我把这笔tx读取出来后,发现其下总共才只有10个vout。是不是出错了,这可能是一个伪造的交易。
然后大于这笔tx索引的vSpent也是同理,正常情况下vout.size和vSpent.size是相等的。
所以会有这个判断:
cpp
if (prevout.n >= txPrev.vout.size() || prevout.n >= txindex.vSpent.size())
4.IsCoinBase
接下来判断,你定位到这笔vout,是否为coinbase交易,也就是Block下的第一笔tx,为什么要判断一下,因为有着不同的处理逻辑。我们来看代码:
cpp
// If prev is coinbase, check that it's matured
if (txPrev.IsCoinBase())
for (CBlockIndex* pindex = pindexBest; pindex && nBestHeight - pindex->nHeight < COINBASE_MATURITY-1; pindex = pindex->pprev)
if (pindex->nBlockPos == txindex.pos.nBlockPos && pindex->nFile == txindex.pos.nFile)
return error("ConnectInputs() : tried to spend coinbase at depth %d", nBestHeight - pindex->nHeight);
因为如果你花费的是coinbase要等到100个确认区块后才能花费,这是为了防止特定的攻击。
关于普通花费为什么不需要。篇幅所限以后单独说一下吧。
这里只需要明白,这是计算是否在100个区块之后,根据这个参数:
cpp
static const int COINBASE_MATURITY = 100;
不符合则拒绝此笔交易。
5.VerifySignature
然后就是大名顶顶的签名验证了:
cpp
// Verify signature
if (!VerifySignature(txPrev, *this, i))
return error("ConnectInputs() : %s VerifySignature failed", GetHash().ToString().substr(0,6).c_str());
具体逻辑也同样不说明,需要很长的篇幅描述,你只需要知道,现在只要调用VerifySignature函数就能完成它。些函数将在后续章节说明。
然后判断,你定位的这笔vout是否被花费了,如果已经花费过了,那是不是你在重复支付。你在花已经用掉的钱,是一种欺骗。所以会有以下判断:
cpp
if (!txindex.vSpent[prevout.n].IsNull())
非空,也就是这笔vout对应的vSpent已经有指向一个tx了。
至此,都通过后,你的引用成立,所以我们来标记一下,建立引用关系。
cpp
txindex.vSpent[prevout.n] = posThisTx;
将txindex下的对应那个vSpent标记为已花费。
注意啊,这里的是改的txindex索引数据,也就是改数据库的索引记录。并不是之前的区块数据啊,区块里也并没有vSpent这个参数,也不可能让你改。那不乱套了吗。
这套数据是单独维护的,用于快速验证交易,但是会有一个问题,如果是单独维护的这个vSpent造假怎么办?如果是假的,但是这个由于只是你本地的数据,你只能欺骗自己的节点,最终链上数据不一致。这个是无法成功的。
6.fMiner
然后是这句:
cpp
// Write back
if (fBlock)
txdb.UpdateTxIndex(prevout.hash, txindex);
else if (fMiner)
mapTestPool[prevout.hash] = txindex;
从执行语句可以看到,修改了txindex后,一个是将它写进数据库,一个是将它写进mapTestPool。
可以分析出分什么要这样,跟上面的一样的原因,如果引用的是同一个区块,那么数据库里还没有,是不能写进数据库的,临时写入到mapTestPool里。
但是这里又是依据什么来判断的呢?
可以看到判断了两个参数fBlock和fMiner,前面我们说过,其它的地方也会调用这个ConnectInputs函数,具体是哪里了,比如这个bool 类型fMiner标志,表示在挖矿,那么我们可以去挖矿函数看一下。
BitcoinMiner函数里,有这么一句:
cpp
map<uint256, CTxIndex> mapTestPoolTmp(mapTestPool);
if (!tx.ConnectInputs(txdb, mapTestPoolTmp, CDiskTxPos(1,1,1), 0, nFees, false, true, nMinFee))
continue;
swap(mapTestPool, mapTestPoolTmp);
第六,第七的参数,传的是false,true,就会写进mapTestPool,说明此区块还在挖矿,上一级已经判断好了,此块还没写进数据库(数据库里没有),所以写到mapTestpool里。
如果是ConnectBlock里调用,则传的是true,false。
我们这里现在主要关注ConnectBlock的逻辑。
写完后,然后会执行这个计数:
cpp
nValueIn += txPrev.vout[prevout.n].nValue;
将这笔tx下的每笔vin,引用的所有vout的值进行累积,因为引用的值不是你能调整的,所以可能发生找零的事,我们需要计算引用的总值是多少,以便后续计算。
7.nTxFee
循环完,得到总值后就会进行这个判断:
cpp
// 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,当得到了nValueIn后,就是你引用的金额,减去实际支付的金额,通过GetValueOut函数获得。
比如,你引用了1btc,要支付给小张0.8btc,那么还剩0.2btc,所以不小于0,第一个判断不成立。
如果小于0,则你没有足够的钱来支付手续费,就会报错。这就是第一个判断的作用。
但这里还有一个问题,那找零呢?所以上面的只是简单的说明,只是为了让你快速理解nTxFee<0的意思。
其实GetValueOut()获取的是所有的vout,当然也包括找零。
那么现在再加上找零,实际是这样的,你引用了1btc,要支付给小张0.8btc,然后找零给自己0.19btc。这样nTxFee就是0.01btc>0,第一个逻辑过了。
但是不是大于0就可以了,矿工还有个最低手续费要求,你不能低于这个手续费,否则,你给它一毛钱也行?那是不可能的。
手续查账不能小于nMinFee,这个参数是函数传进来的,在挖矿时计算,会传入,具体算法,等到了挖矿函数再说明。如果不是挖矿时,说明是接受的别人广播的块,所以不用传入参数,因为已经计算判断好了,在这里不需要nMinFee限制(函数声明不传参默认为0),你只需要计算手续费就行。
8.nFee
接下来是:
cpp
nFees += nTxFee;
累积手续费,为什么要这样,因为这只是一笔tx的手续费,矿工的手续费是按一个区块收取的,打包也是按区块打包的。所以这一切是非常的合理。计算这个区块最终能获得多少手续费。
9.写入CTxIndex
手续费算好后,最后,执行这段语句:
cpp
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的交易索引,根据情况,如果非挖矿,就写进数据库里面,会在AddTxIndex里面根据传进来的参数,构建CTxIndex,然后写入数据库,代码如下:
cpp
bool CTxDB::AddTxIndex(const CTransaction& tx, const CDiskTxPos& pos, int nHeight)
{
assert(!fClient);
// Add to tx index
uint256 hash = tx.GetHash();
CTxIndex txindex(pos, tx.vout.size());
return Write(make_pair(string("tx"), hash), txindex);
}
如果是fMiner还在挖矿,这个索引临时存到mapTestPool里,并且它的位置是指定的格式,是
CDiskTxPos(1,1,1),完整代码如下:
cpp
else if (fMiner)
{
// Add transaction to test pool
mapTestPool[GetHash()] = CTxIndex(CDiskTxPos(1,1,1), vout.size());
}
至此,整个ConnectInputs函数代码逻辑我们都了解了,那么回到开头的第一句:
cpp
if (!IsCoinBase())
为什么会有这个判断,都清楚了,因为CoinBase交易是没有引入的上级vout的,所以也不用构建引用的索引,和验证那笔vout是不是属于你的这些逻辑。
好了,那么接下来,我们看一下,如果我们只要写入交易索引该怎么做?
目前只保留两个东西就可以了,一个是在ConnectBlock函数,循环遍历vtx,调用ConnectInputs,一个是在ConnectInputs函数,只调用这个函数:
cpp
if (!txdb.AddTxIndex(*this, posThisTx, nHeight))
直接写数据库就行了,因为我们只有一个区块,只测试写交易索引,所以不用验证,以及维护引用的区块交易索引等这些事。
但是我们返回上级ConnectBlock函数可以看到这样遍历写完之后(跳回CBlock),还会有一些操作,是这部分代码:
cpp
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);
return true;
我们来一个一个说明。
我们知道一个tx的GetValueOut就是获得这个tx下的vout总和,而vtx[0],就是第一笔交易,就支付给矿工的,也coinbase,那么vtx[0].GetValueOut就是获取支付给矿工的费用(包含挖矿奖励和手续费)
10.GetBlcokValue
那么GetBlcokValue(nFees)获取的是什么呢?需要看具体的代码,如下:
cpp
int64 CBlock::GetBlockValue(int64 nFees) const
{
int64 nSubsidy = 50 * COIN;
// Subsidy is cut in half every 4 years
nSubsidy >>= (nBestHeight / 210000);
return nSubsidy + nFees;
}
看到没有,这个nFees就是之前计算出的整个区块的交易手续费。
然后在这里还要加上矿工基础奖励,这里有这样一个计算公式:
cpp
// Subsidy is cut in half every 4 years
nSubsidy >>= (nBestHeight / 210000);
根据当前区块高度nBestHeight来计算的,初始值是50个币,每4年减半就是这么来的。
这就是这个函数获得的值。
然后回到上面的if逻辑,就是为了防止矿工给自己多vout,胡乱构造,所以你的vout必须符合计算规则。如果不符合,直接返回false。
11.hashNext
然后是这里,交易索引完成后,就会解决区块索引的一些事:
cpp
if (pindex->pprev)
{
CDiskBlockIndex blockindexPrev(pindex->pprev);
blockindexPrev.hashNext = pindex->GetBlockHash();
txdb.WriteBlockIndex(blockindexPrev);
}
就是这个区块索引的父索引存在,则将父索引的hashNext指向自己的哈希,设置一下关系。
更改完后,将父索引写入数据库。(注意这里用的是CDiskBlockIndex,hashNext是实值,所以只写磁盘(数据库)),非写内存索引。
12.AddToWalletfMine
然后是这部分:
cpp
// Watch for transactions paying to me
foreach(CTransaction& tx, vtx)
AddToWalletIfMine(tx, this);
当一个区块成功连接到区块链后,就会遍历这个区块的每一笔交易,查看是否有支付给我的交易,如果有,就会记录到钱包数据库里。
这就是之前说的,两部分交易数据,钱包有一份单独的关于我的交易数据,CTxDB里也有一份(所有人的),就是在这部分代码写入的。
好,至此ConnectBlock函数的代码逻辑也了解完了。
在下一章中,我们把这些函数复制到我们的项目,并修改代码,以测试一下读写交易索引。