先来看一则旧闻:
图3-1 关于比特币内存池的旧闻这里说的比特币内存池,里面都是还没有被纳入区块链中的交易,因此也叫交易池。当用户使用比特币发起交易之后,会通过P2P网络广播发送到各个节点,每个节点再把这个交易放入交易池中。随后矿工挖矿、也就是生成区块的时候,再从交易池中选择一部分交易进行打包。当区块生成以后,相关的交易就从交易池中移除。因此,可以想象,当交易特别火爆的时候,交易数量猛增,但是区块生成的速度仍然是稳定的,就可能会导致大量交易积压,长期得不到处理,这就会出现上图中的情况了。
那么问题来了,既然要从交易中选择一部分进行打包,那到底选谁、不选谁?依据什么来定?很简单,比特币规定,每个交易当中,所有的输入总和必须大于等于所有的输入总和,注意这里是大于等于!也就是说,你可以输入101个比特币,输出的只有100个比特币,可以吗?当然可以!那么多出的1个比特币哪里去了?答案是作为交易手续费,奖给了挖矿的矿工。再回到刚才的问题,矿工选哪个交易、不选哪个交易主要看什么?很显然,看哪个交易给的手续费更高呗!
因此,小伙伴们,如果你希望你的交易能够更优先地被打包确认,那就要记得适当提高交易手续费。目前主要的交易平台都会给出一个手续费参考值,基本上按平均值给可以了。那么,这个费用怎么计算?是不是交易数额越大,手续费就越大呢?胖兔告诉你,不是的,手续费只跟交易本身的大小有关系。什么叫交易的大小?大体上就是把所有的输入和输出加起来,一共占多少个字节。如果你一次转1000个BTC,但只用了一个UTXO,交易大小就会很小;而说不定你才转1个BTC,但用了10个UTXO,那对不起,交易大小反而上去了,极端情况下,你转账的金额可能还没有你要付的手续费高!
图 3-2 每日交易费用走势图上图是blockchain给出的每日交易手续费走势图,截止本文写作时,每天的手续费总额是16.57个比特币。最高点发生在2017年12月22号,当天的手续费达到了1495个比特币!对比特币的价值没有概念?再来看下图:
图3-3 单个交易费用走势图这是每个交易的平均手续费,单位是美元。当前每个交易平均要付手续费76.54美元,最高峰的时候要付160美元!这个手续费用可不便宜哦!
图3-4 交易池里交易总数上图是比特币交易池里交易总数量变化走势图。现在交易池中的数量保持在2700到2800个左右,峰值最高的时候在2017年12月,差不多正式比特币最值钱的时候,交易总数走过18万个!这么多交易,那交易池一定很大喽!看看下面的图:
图3-5 交易池大小走图可以看到交易池最大的时候曾经达到了139M,不过相对于当前的电脑内存容量来说,这其实根本不算什么,一部手机都可以轻松搞定。
好了,下面进入源码时间。
交易池的代码在txmempool.h/cpp里定义的,主要涉及两个类,CTxMemPoolEntry和CTxMemPool两个类。
CTxMemPoolEntry
CTxMemPoolEntry指的是交易池中的单个条目,包含一条交易以及相关的数据。其数据声明部分是这样的:
图3-6 CTxMemPoolEntry数据成员注意这里的数据成员全部是私有的。所有数据在构造函数里就被赋值或计算出来,之后只能通过类成员函数去修改。
tx就是交易指针,指向当前条目所对应的交易。
nFee就是交易手续费。
nTxWeight是交易重量,这是2017年比特币引入隔离见证时增加的,隔离见证到将来再研究。
nUsageSize是交易的内存使用量。这3个数据都是缓存用来便于统计计算的。
nTime是交易被加入交易池的时间。
entryHeight是加入交易池时,区块链的高度。
spendsCoinbase指的是这个交易是否花费了某个币基(Coinbase),所谓Coinbase就是挖矿时对矿工的奖励,比特币规定凡是成功挖到某个区块的矿工,除了交易费之外,还有额外奖励。2009年比特币刚出来的时候,挖到一个块能奖励50个比特币,之后每4年减半,现在只有12.5个比特币了。
sigOpCost是签名操作消耗,也就是这个交易的签名需要多少条指令。关于签名脚本,将来再详细研究。
feeDelta是手续费增量,增加后的手续费就是nFee + feeDelta,它的目的是为了在挖矿时调整交易优先级,因此这个值在初始化的时候肯定是0。
lockPoints是锁定点。所谓锁定点,可以是区块高度,也可以是时间,表示只有当达到一定的区块高度,或是到达某个时间点以后,这个交易才能被打包进区块。
接下来的两组数据,分别对应子孙交易和祖先交易。所谓子孙交易,就是依赖当前交易的后续交易,比如交易A是张三给李四10个BTC,交易B是李四再拿这这10个BTC给了王五8个BTC,那么交易B就是交易A的子孙,交易A就是交易B的祖先。如果交易A没有被选中打包,交易B自然就只能跟在后面等。
nCountWithDescendants、nSizeWithDescendants、nModFeesWithDescendants分别指的是当前交易以及所有子孙交易,它们的总数量、总大小和总手续费。
nCountWithAncestors、nSizeWithAncestors、nModFeesWithAncestors、nSigOpCostWithAncestors分别指的是当前交易以及所有子孙交易,它们的总数量、总大小、总手续费和总签名消耗。
CTxMemPool
交易池的实现在比特币源码里算是比较复杂的部分了,光是类声明之前的注释就有70行,而且全部是巨长的句子,让当年连滚带爬才过英语四级的胖兔看得郁闷无比、几欲吐血。
由于CTxMemPool里的数据和成员函数实在太多,逐个介绍实在太累,还是只挑关键地来说吧(瘦兔:别听他忽悠,其实很多连胖兔自己都还没搞懂~)。源码不多贴了,小伙伴们可以自己对照着阅读理解。
mapTx。这是第一个重要的数据成员,也是CTxMemPoolEntry的集合,它的数据类型是indexed_transaction_set,这个类型是这样定义的:
图3-7 索引化交易集合的类型声明这里使用了boost库的multi_index_container,它是个多索引容器,允许对一系列数据添加之个索引。容器里的元素类型就是CTxMemPoolEntry,并给它一共建了4个索引:首先是交易ID其实就是交易的hash;接着是手续费比例,也就是手续费除以交易大小,注意这里的手续费比是包含子孙交易的,CompareTxMemPoolEntryByDesendantScore是一个自定义的比较手续费比例的运算子;第三个是进入交易池的时间;最后一个是包含祖先交易的手续费比例。从前面的图中我们可以知道,交易数量最多的时候可达18万个,但有这4个索引,绝大多数情况下搜索交易时,都可以非常迅速地找到。
vTxHashes。用于存放所有的隔离见证hash。它的类型是std::vector<std::pair<uint256, txiter> >,这是一个vector,每个元素又是一个pair,pair的第一个元素就是见证hash,第二个元素类型txiter的定义是indexed_transaction_set::nth_index<0>::type::const_iterator,其实就是mapTx第一个索引的迭代子,通过它可以遍历所有的CTxMemPoolEntry,也就是遍历交易。
mapLinks。用于存放与每个交易相关的父、子节点。它的类型是std::map<txiter, TxLinks, CompareIteratorByHash>,其中的TxLinks是一个结构,包含parents、children两个数据成员,这两个成员的类型叫setEntries,是一个txiter的集合(std::set)。注意这个数据是私有的,它只在类内部使用,主要是加快访问上、下级节点的速度。
mapNextTx。它的类型是indirectmap<COutPoint, const CTransaction*>,indirectmap是在比特币里自定义的数据类型,是为了方便比较指针所指向的数据。我们知道,每个交易的输入其实都来自于之前的某个输出,mapNextTx存放是从每个COutPoint到后续交易的链接。它与mapLinks的区别,mapLinks里所指向的父子交易都还在交易池里,但mapNextTx里的COutPoint有可能已经被写入区块。
mapDeltas。存放的是每个交易所对应的交易费增量。
其他几个重要的私有数据成员,主要有:
nCheckFrequency,代表在2^32秒内,一共需要检查几次交易池。
nTransactionsUpdated,当前已更新多少交易,用于触发挖矿操作。
minerPolicyEstimator,是CBlockPolicyEstimator类变量,用于挖矿策略评估。
totalTxSize,所有交易的大小,这里没有包含见证数据,与交易被存储时所占用的大小是不一样的。
cachedInnerUsage,所有map成员的元素累加起来,所占用的动态内存。包括mapTx,mapNextTx,mapDeltas等。
lastRollingFeeUpdate,上一次调整费率的时间。
blockSinceLastRollingFeeBump,上一次调整费率之后是否生成过区块。
rollingMinimumFeeRate,上一次调整的最小费率。
最后介绍一下比较重要的成员函数:
check与setSanityCheck,setSanityCheck用于设置对交易池进行健全性检查的频率。check则是对整个交易池进行检查,包括:每个交易的输入是否都是现有UTXO或者来自交易池中其他交易的输出;每个交易输入都在mapNextTx中;祖先交易状态正常;能通过mapNextTx找到子孙交易;另外还要对交易的大小、内存占用进行检查。注意这里的检查可以防止双花交易,我们知道每个输出只能被用作一次输入,用完了就会被销毁,但如果一个输出被用了两次,就被称为双重支付或者叫双花,这是数字货币必须要解决的问题。
addUnchecked、removeUnchecked就是添加或移除新的交易。
HasNoInputsOf用来检查一个交易的所有输入,是否都不在交易池中。也就是说所有祖先交易都已经打包进区块了。
PrioritiseTransaction、ApplyDelta、ClearPrioritisation用于调整交易优先级,一般在挖矿时使用。
RemoveStaged用于从交易池中移除一部分交易。这里移除的原因可能是过期、大小受限、重组、打包入区块、冲突、被替代等等。一个交易被移除后,它相关的祖先和子孙交易中的数据,都会被连带更新。
UpdateTransactionsFromBlock,受挖矿机制的影响,有时候矿工会提前挖出后面的区块,但因为没有链入主链,这个区块中包含的交易会被重新放入交易池,这时它们的子孙交易可能已经在交易池中了,UpdateTransactionsFromBlock将会找到这些子孙交易,并更新相关数据。
CalculateMemPoolAncestors、CalculateDescendants,当一个交易被放入内存池时,CalculateMemPoolAncestors用于找到它的所有祖先交易,CalculateDescendants找到所有子孙交易。
GetMinFee用于计算加入交易池所需要的最小费用。
TrimToSize用于调整交易池大小,此时可能会移出一部分交易(原因设为大小受限)。
Expire用于将所有早于指定时间的交易,全部设定为过期。
交易池是比特币中非常重要的部分,这一块不啃下来,后面许多内容都不容易理解。好吧,这次就到这里。下一章准备开始琢磨挖矿。
本文原创作者胖兔(魏兆华),首次发表于简书,欢迎转载,请注明出处。
网友评论