这篇文章总结下比特币挖矿和共识相关内容。
挖矿这个词可能让一部分人误解了比特币矿工。矿工的工作,主要是验证交易和打包区块,而得到的比特币奖励和手续费是一种激励手段和“副产品”。比特币的挖矿是比特币P2P网络节点各自独立发生的四个过程的相互作用的结果:
前文提到过,比特币系统设计中没有账户余额,取而代之的是历史交易记录,比特币完整节点可以通过历史交易记录计算得到UTXO。当比特币钱包“收到”比特币时,实际发生的是钱包检测到UTXO池中存在该钱包私钥可以花费的UTXO。因此,用户的比特币“余额”实际是用户钱包可以花费的所有UTXO的和,而这个值需要从很多区块、很多交易记录中汇总得到。余额的概念存在于钱包,但不存在于区块链。
首先回顾一下交易结构,一笔交易的输入包含以下信息:
交易的输入需要有一个哈希指针,指向即将在交易中花费的UTXO。节点在向邻居节点转发交易前,会首先独立验证交易,只有符合一系列条件的交易才会在比特币网络向新的节点进行传播。矿工独立验证交易的内容,一部分内容是验证UTXO是否合法(防止双重支付),另一部分就是验证交易脚本是否有权限花费这些UTXO(验证签名)。验证交易输入的代码主要在以下几个函数中:AcceptToMemoryPool
, Check Transaction
, CheckInputs
。
bool CheckInputs(const CTransaction& tx, CValidationState &state, const CCoinsViewCache &inputs, bool fScriptChecks, unsigned int flags, bool cacheSigStore, bool cacheFullScriptStore, PrecomputedTransactionData& txdata, std::vector *pvChecks)
{
if (!tx.IsCoinBase())
{
if (pvChecks)
pvChecks->reserve(tx.vin.size());
if (fScriptChecks) {
// 此处省略部分代码
for (unsigned int i = 0; i < tx.vin.size(); i++) {
// 1. 验证UTXO,防止双重支付
const COutPoint &prevout = tx.vin[i].prevout;
const Coin& coin = inputs.AccessCoin(prevout);
assert(!coin.IsSpent());
// 2. 验证签名,确认所有权
CScriptCheck check(coin.out, tx, i, flags, cacheSigStore, &txdata);
if (pvChecks) {
pvChecks->push_back(CScriptCheck());
check.swap(pvChecks->back());
} else if (!check()) {
if (flags & STANDARD_NOT_MANDATORY_VERIFY_FLAGS) {
CScriptCheck check2(coin.out, tx, i,
flags & ~STANDARD_NOT_MANDATORY_VERIFY_FLAGS, cacheSigStore, &txdata);
if (check2())
return state.Invalid(false, REJECT_NONSTANDARD, strprintf("non-mandatory-script-verify-flag (%s)", ScriptErrorString(check.GetScriptError())));
}
return state.DoS(100,false, REJECT_INVALID, strprintf("mandatory-script-verify-flag-failed (%s)", ScriptErrorString(check.GetScriptError())));
}
}
if (cacheFullScriptStore && !pvChecks) {
scriptExecutionCache.insert(hashCacheEntry);
}
}
}
return true;
}
矿工会将通过验证的交易放入内存(代码见AcceptToMemoryPool),在进行工作量证明计算并赢得记账权后,将上述交易打包到区块中。
static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool& pool, CValidationState& state, const CTransactionRef& ptx, bool* pfMissingInputs, int64_t nAcceptTime, std::list * plTxnReplaced, bool bypass_limits, const CAmount& nAbsurdFee, std::vector & coins_to_uncache)
{
// 此处省略部分代码
GetMainSignals().TransactionAddedToMempool(ptx);
return true;
}
假设矿工的名字是Jing。在Alice付给Bob比特币购买咖啡时,Jing的节点组装了一条区块链,高度是277314。Jing的节点持续监听网络中的交易,也持续监听其他节点发现的新区块。突然Jing的节点在网络中发现了区块277315,这时候对应区块277315的算力竞赛宣告结束,同时开启了对应区块277316的算力竞赛。
在Jing的节点搜索区块277315的答案的时候,它同时收集了许多通过验证的交易,并把这些通过验证的交易放到了内存池中。一旦收到区块277315并且完成了验证,Jing的节点会比较内存池中的所有交易,删除区块277315中包含的交易。内存池中剩下的交易记录可以继续保留,等待下一次的交易打包。
Jing 的节点立刻创建一个空区块,称为候选区块277316。一旦候选区块在工作量证明算法中胜出,这个候选区块就成为了合法区块。回顾下区块的结构,比特币区块主要包含四个字段:prevHash
, nonce
, Merkle Root
和timestamp
。
构成区块的过程主要分成两个部分:1. 汇总交易,生成交易摘要Merkle Root哈希值 2. 加入随机数nonce,生成区块header,并验证区块header是否满足上个区块的设定的目标值;
每个区块的第一笔交易叫做Coinbase交易。Coinbase交易是一笔特殊交易,因为它包含了挖矿奖励。和普通交易不同,coinbase交易的输入不包含UTXO,而是输入一个coinbase值,用于凭空创造比特币。Coinbase交易有一个输出,向矿工的比特币地址支付比特币。
我们可以通过命令bitcoin-cli getrawtransaction
查看coinbase。
$ bitcoin-cli getrawtransaction
d5ada064c6417ca25c4308bd158c34b77e1c0eca2a73cda16c737e7424afba2f 1
{
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0f03443b0403858402062f503253482fffffffff0110c08d9500000000232102aa970c592640d19de03ff6 f329d6fd2eecb023263b9ba5d1b81c29b523da8b21ac00000000 ",
"txid": "d5ada064c6417ca25c4308bd158c34b77e1c0eca2a73cda16c737e7424afba2f",
"version": 1,
"locktime": 0,
"vin": [{
"coinbase": "03443b0403858402062f503253482f",
"sequence": 4294967295
}],
"vout": [{
"value": 25.09094928,
"n": 0,
"scriptPubKey": {
"asm": "02aa970c592640d19de03ff6f329d6fd2eecb023263b9ba5d1b81c29b523da8b21OP_CHECKSIG",
"hex": "2102aa970c592640d19de03ff6f329d6fd2eecb023263b9ba5d1b81c29b523da8b21ac",
"reqSigs": 1,
"type": "pubkey",
"addresses": [
"1MxTkeEP2PmHSMze5tUZ1hAV3YTKu2Gh1N"
]
}
}]
}
Coinbase交易不包含解锁脚本字段(即scriptSig),取代它的是coinbase数据字段,其大小限制为2至100个字节。除了最前面的几个字节,剩下的coinbase数据字段内容可以由矿工随意填写。例如,著名的比特币创世区块上,中本聪写下了如下文字:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。现在,矿工一般在coinbase数据字段写入其所在矿池的名字。
挖矿成功后,矿工可以得到多少奖励呢?两部分,区块奖励+手续费。上面的例子中,矿工奖励总数25.09094928枚比特币,其中包含25个比特币的区块奖励和0.09094928个比特币的手续费。比特币区块链每生成210000个区块,约4年时间,区块奖励减半(参数nSubsidyHalvingInterval
,见src/chainparam.cpp
中)。到2140年,区块奖励将归零,矿工的激励将只剩交易手续费。
CAmount GetBlockSubsidy(int nHeight, const Consensus::Params& consensusParams)
{
int halvings = nHeight / consensusParams.nSubsidyHalvingInterval;
// Force block reward to zero when right shift is undefined.
if (halvings >= 64)
return 0;
CAmount nSubsidy = 50 * COIN;
// Subsidy is cut in half every 210,000 blocks which will occur approximately every 4 years.
nSubsidy >>= halvings;
return nSubsidy;
}
Coinbase交易生成后,剩下的工作就是将待确认的交易组织起来,生成一棵Merkle树作为交易摘要写进区块277316。交易打包到Merkle Tree过程详见本系列第一篇文章比特币源码阅读笔记【基础篇】。最终生成一个候选区块,格式如下所示。
$ bitcoin-cli getblockhash 277316
0000000000000001b6b9a13b095e96db41c4a928b97ef2d944a9b31b2cc7bdc4
$ bitcoin-cli getblock 0000000000000001b6b9a13b095e96db41c4a928b97ef2d944a9b31b2cc7bdc4
{
"hash" : "0000000000000001b6b9a13b095e96db41c4a928b97ef2d944a9b31b2cc7bdc4",
"size" : 218629,
"height" : 277316,
"version" : 2,
"merkleroot" : "c91c008c26e50763e9f548bb8b2fc323735f73577effbc55502c51eb4cc7cf2e",
"tx":[
"d5ada064c6417ca25c4308bd158c34b77e1c0eca2a73cda16c737e7424afba2f",
"b268b45c59b39d759614757718b9918caf0ba9d97c56f3b91956ff877c503fbe",
... 后面交易省略 ...
],
"time" : 1388185914,
"nonce" : 924591752,
"bits" : "1903a30c",
"difficulty" : 1180923195.25802612,
"chainwork" : "000000000000000000000000000000000000000000000934695e92aaf53afa1a",
"previousblockhash" : "0000000000000002a7bbd25a417c0374cc55261021e8a9ca74442b01284f0569"
}
比特币的区块链结构主要有两层哈希,第一层是在区块的链结构上,每个区块header都包含一个哈希指针,指向前一个区块;第二层是在每一个区块内,区块内部打包的所有交易用Merkle树组织起来,如下图所示
回顾下区块header的结构:
version
,软件版本号,由当前客户端版本决定;父区块哈希值prevHash
,由该节点可以获取的最新区块决定;区块创建时间timestamp
,即矿工节点的时间戳;这三个参数没有太多可以解释的内容。
后面三个参数:PoW目标值target
,计数器nonce
,交易摘要值merkle root
是达成共识的关键。
什么是目标,什么又是目标难度呢?目标是比特币网络中所有矿工节点一起计算的那道题目,题目的格式为: 找到一个区块,它的header哈希值小于0000000000000000029AB9000000000000000000000000000000000000000000。这个题目就是下一个区块的挖矿目标,而这个数值0000000000000000029AB9000000000000000000000000000000000000000000就是所谓的目标难度。那么上面例子中的哈希计算有多难呢?网络中的一个节点如果想求出这个哈希值,平均需要进行1.8 * 10 ^ 21次哈希计算。这个难度是动态调整的,假如网络中矿工节点的计算能力都很强,那么难度将会提高。目标难度由下面的公式计算
目标 = 系数 * 2^(8 * (指数 – 3))
根据这个公式,假如难度位取值是0x1903a30c, 我们可以得到目标值:
目标 = 0x03a30c * 2^(0x08 * (0x19 - 0x03))^
=> 目标 = 0x03a30c * 2^(0x08 * 0x16)^
=> 目标 = 0x03a30c * 2^0xB0^
用十进制表示:
=> 目标 = 238,348 * 2^176^
=> 目标 =
22,829,202,948,393,929,850,749,706,076,701,368,331,072,452,018,388,575,715,328
切换回十六进制表示:
=> 目标 = 0x0000000000000003A30C00000000000000000000000000000000000000000000
我们可以看到,哈希目标决定了求解PoW的时间。为什么要调整难度呢,谁在调整难度,又是如何调整难度呢?
首先,比特币平均每10分钟生成一个区块,这是比特币的基础设定。比特币区块产生速率不仅需要短期保持恒定,而且需要维持多年。在这个过程中,计算机的算力将持续提升,加入挖矿的计算机也将持续变化,不管外部条件如何改变,比特币区块生成时间都将动态地调整到10分钟。在出块时间上保持稳定是一种聪明的设计,2100万个比特币,在2140年全部挖矿得出,这个社会学实验周期在100年以上,无论是成功还是失败,过程中的起起伏伏已经是极佳的社会学实验案例。
完全去中心化的网络中如何完成难度调整呢?我们可以在链参数定义的源代码chainparams.cpp
中找到参数:nPowTargetTimespan=14*24*60*60
,两周调整一次PoW难度。按每10分钟产生一个区块计算,两周产生(14*24*60*60)/(10*60)=2016
个区块,也就是每产生2016个区块调整一次难度。
class CMainParams : public CChainParams {
public:
CMainParams() {
strNetworkID = "main";
consensus.nSubsidyHalvingInterval = 210000;
// 省略部分参数
consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks
consensus.nPowTargetSpacing = 10 * 60;
consensus.fPowAllowMinDifficultyBlocks = false;
consensus.fPowNoRetargeting = false;
consensus.nRuleChangeActivationThreshold = 1916; // 95% of 2016
consensus.nMinerConfirmationWindow = 2016; // nPowTargetTimespan / nPowTargetSpacing
// 隔离见证 (BIP141, BIP143, and BIP147)
consensus.vDeployments[Consensus::DEPLOYMENT_SEGWIT].bit = 1;
consensus.vDeployments[Consensus::DEPLOYMENT_SEGWIT].nStartTime = 1479168000; // November 15th, 2016.
consensus.vDeployments[Consensus::DEPLOYMENT_SEGWIT].nTimeout = 1510704000; // November 15th, 2017.
// The best chain should have at least this much work.
consensus.nMinimumChainWork = uint256S("0x000000000000000000000000000000000000000000723d3581fe1bd55373540a");
}
};
调整难度的时候会评估最后产生的2016个区块的时间,与2016 * 10分钟做比较,实际时间除以预期时间的比例决定了下一次的目标难度。重新定义哈希值目标的公式为:
新目标 = 旧目标 * (产出过去2016个区块的时间 / 20160 分钟)
计算下次PoW工作量(目标难度)的代码,见src/pow.cpp
中的CalculateNextWorkRequired函数:
unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, int64_t nFirstBlockTime, const Consensus::Params& params)
{
if (params.fPowNoRetargeting)
return pindexLast->nBits;
// Limit adjustment step
int64_t nActualTimespan = pindexLast->GetBlockTime() - nFirstBlockTime;
if (nActualTimespan < params.nPowTargetTimespan/4)
nActualTimespan = params.nPowTargetTimespan/4;
if (nActualTimespan > params.nPowTargetTimespan*4)
nActualTimespan = params.nPowTargetTimespan*4;
// Retarget
const arith_uint256 bnPowLimit = UintToArith256(params.powLimit);
arith_uint256 bnNew;
bnNew.SetCompact(pindexLast->nBits);
bnNew *= nActualTimespan;
bnNew /= params.nPowTargetTimespan;
if (bnNew > bnPowLimit)
bnNew = bnPowLimit;
return bnNew.GetCompact();
}
我们可以用下面的Python代码表示工作量证明的计算过程,两层循环尝试merkle root
和nonce
, 生成一个区块;将区块和目标难度输入PoW算法进行验证,哈希值满足难度target要求则验证通过,见下面Python代码第37行。
#!/usr/bin/env python
# example of proof-of-work algorithm
import hashlib
import time
max_nonce = 2 ** 32 # 4 billion
def proof_of_work(header, difficulty_bits):
# calculate the difficulty target
target = 2 ** (256-difficulty_bits)
for nonce in xrange(max_nonce):
hash_result = hashlib.sha256(str(header)+str(nonce)).hexdigest()
# check if this is a valid result, below the target
if long(hash_result, 16) < target:
print "Success with nonce %d" % nonce
print "Hash is %s" % hash_result
return (hash_result,nonce)
print "Failed after %d (max_nonce) tries" % nonce return nonce
if __name__ == '__main__':
nonce = 0
hash_result = ''
# difficulty from 0 to 31 bits
for difficulty_bits in xrange(32):
difficulty = 2 ** difficulty_bits
print "Difficulty: %ld (%d bits)" % (difficulty, difficulty_bits)
print "Starting search..."
# checkpoint the current time
start_time = time.time()
# make a new block which includes the hash from the previous block
# we fake a block of transactions - just a string
new_block = 'test block with transactions' + hash_result
# find a valid nonce for the new block
(hash_result, nonce) = proof_of_work(new_block, difficulty_bits)
# checkpoint how long it took to find a result
end_time = time.time()
elapsed_time = end_time - start_time
print "Elapsed Time: %.4f seconds" % elapsed_time
if elapsed_time > 0:
# estimate the hashes per second
hash_power = float(long(nonce)/elapsed_time)
print "Hashing Power: %ld hashes per second" % hash_power
每个矿工的谜题都是一样的吗?不,原因主要有几点:
下一步是矿工独立地将区块组装到区块链。每个矿工节点基于PoW算法,独立选择计算工作量最大的链,并将交易组装进链。
比特币分布式共识机制的最后一步就是将区块组装到链并选择最多工作量的链。一旦一个节点验证了一个新的区块,它将会尝试把这个区块组装到现有的链中。节点维护了三个区块集合:
上述三种区块都必须通过交易验证和区块验证,而验证失败的区块会被抛弃,不会向网络中其他节点转发。主链指的是在一个时刻拥有最多累积工作量的链,大多数情况下主链就是拥有最多区块的链,除非此刻有两条长度相同的链,其中一条拥有更多的累积工作量。主链还会维护分支链用于未来引用,因为分支链有可能得到更多的后续区块从而成为新的主链。当矿工挖出新区块时,该节点通过选择扩展哪条链来投票。我们不用担心部分矿工故意选择工作量更少的链,因为多数矿工会选择工作量最多的链,而工作量较少的链会被彻底放弃。打个比方,超市收银台有多个结账窗口,只有排队人数最多的收银台可以结账。这时候为了结账,矿工只能选择最长的队伍排队,因为短队伍意味着不会得到结账机会。
如果收到了一个合法区块,但是没有在主链和分支链中发现该区块的父区块,那么这个区块就叫做孤块。因为孤块是合法的区块,只是没有找到父区块,所以孤块也会被节点保存下来,存储在孤块池中,直到找到它的父区块。一旦找到了父区块,孤块就离开孤块池,链到已经存在的链上。一般来说,两个区块在很短时间内产生,并且被其他节点接收的顺序刚好和产生顺序相反才会出现孤块,孤块是一种临时现象。
因为区块链是一个分布式的数据结构,所以不同的副本不会永远一致。区块可能在不同时间到达不同的节点,造成各个节点拥有不同的区块链视角。为了解决这个问题,每个节点选择承认并扩展一条最多工作量的链。只要所有节点都选择最多累积工作量的链,那么比特币网络将最终收敛到一致状态。节点之间区块链版本不一致的状态称作分叉,一般来说分叉只是一种临时状态,当出现更多区块的链时,网络节点会达成新的一致状态。下面我们解释一下网络分叉和合并的过程。
下图中蓝色的圈代表比特币矿工节点,黑色的线代表节点之间的网络连接关系,五角星代表最新的一个区块。假设初始状态时所有节点的最新区块都是这个五角星。
下一时刻,节点X和节点Y几乎同时挖矿得到了两个不同的区块,节点X挖出了米白色的正三角形,节点Y挖出了橙色的倒三角形,两个区块被X和Y各自追加在五角星后面,并开始向各自连接的矿工节点广播,见图中的红色箭头。收到节点X挖出三角形区块的节点将三角形区块追加到自己的区块链上,同样地,收到节点Y挖出倒三角形区块的节点将倒三角形区块追加到自己的区块链上,这时候,比特币网络产生了临时的分叉。
随着节点在网络中同步区块数据,每个节点都会收到两个合法的区块,两个区块都会存在于该节点维护的区块链上。
这时候,一个矿工节点挖出了新的区块–绿色方块,这个矿工选择将绿色区块连接在米白色三角形之后,并向网络中传播。随着绿色区块的传播,当绿色区块传播到节点Y的时候,节点Y将绿色区块追加在米白色的区块之后。这里比较有趣,虽然节点Y挖出了橙色的区块,但是当它收到绿色区块后依然需要把绿色区块追加在米白色区块上,而不能忽略绿色区块。这是游戏规则。
假如矿工有能力修改挖矿策略,形成了一批破坏者,只保留向自己挖出的区块追加的块,丢弃其他节点。这种情况会发生什么呢?短期来看,比特币网络节点行成了两派,各自维护自己的区块,如果两派区块链都继续沿着各自路线前进,并且都有其支持者,那么比特币将分裂成两种币。
回到游戏规则中来,节点Y需要追加绿色区块到节点X挖出的米白色三角形后面,随着网络节点同步区块的进行,“五角星-米白色三角形-绿色方块”这条链成为拥有最多累计工作量的区块链,成为主链。在比特币的世界里橙色的区块很悲剧,只能认赔,什么也得不到。这时候比特币网络再次成为统一状态。
顺便提一句,在以太坊的世界里,橙色的区块称为叔块,如果叔块得到确认还可以得到奖励,称为叔块奖励。造成这种现象的原因是,比特币的出块时间是固定的10分钟,而以太坊的出块时间是变化的,大体出块时间如下:
在第 4000000个区块,总的以太币数量大约是 93262556,时间大约是 2017-08-15 区块时间为 30.01秒。
在第 4500000个区块,总的以太币数量大约是 95912556,时间大约是 2018-11-03 区块时间为 136.71秒。
在第 5000000个区块,总的以太币数量大约是 98562556,时间大约是 2025-10-02 区块时间为 835.81秒。
现阶段以太坊的出块时间只有30秒,相比于10分钟很短。网络传输过程中,由于种种因素,会出现网络孤岛,叔块比例会远高于比特币的场景。为了弥补这些矿工的损失,以太坊设立了叔块奖励,详见幽灵协议(Greedy Heaviest-Observed Sub-Tree, 简写为GHOST)。
2017年初,比特币社区发生了一场大的变革,其中几个关键词就是:隔离见证,区块扩容,闪电网络。它们出现的原因都是为了解决比特币网络交易拥堵的问题,只是技术路线出现了分歧。矿工为首的一派的意见是扩容区块,而比特币Core团队坚持1MB区块大小,双方互不妥协,最终比特币分叉成了两种币,BTC和BCH。
隔离见证是指一种比特币架构修改方案,它会影响到比特币的扩展性、安全性、性能和经济激励等多个方面。隔离见证方案的原文可以查看以下几篇BIP:
鉴于篇幅所限,本篇文章不再展开比特币扩容问题。感兴趣的读者敬请期待–比特币源码阅读笔记【扩容篇】。