比特币共识模块源码分析
前言
分析比特币系统的共识模块,包括共识算法、交易的流程等,基于比特币开源客户端Bitcoin Core v22.0 版本的源码。
比特币中共识算法的大体步骤如下:
新交易被创建并广播到比特币网络中。
每个节点接收到交易后,独立地对交易进行验证。
矿工节点将新交易收集到一个区块中,并为该区块寻找工作量证明,然后将新区块广播到网络中。
每个节点收到区块后,对区块进行独立的校验,并组装进区块链中。
每个节点对区块链进行独立选择,选择最大工作量证明的链。
接下来,将结合源码讲解分析上述步骤。
创建交易
在Bitcoin Core客户端中,用户可以通过 $ bitcoin-cli createrawtransaction
命令创建一笔交易。这个命令背后对应着一个RPC接口,并映射到一个处理函数。映射关系定义在src\rpc\rawtransaction.cpp
中:
1 | // 注册交易的RPC接口及其对应的处理函数 |
该注册函数的被调用关系图如下:
创建交易RPC接口对应的处理函数如下:
1 | static RPCHelpMan createrawtransaction() |
函数定义了命令的说明、请求参数、返回值与内部实现过程,主要是根据请求参数构造一笔交易,然后返回交易的哈希值。构造的过程主要包括构造交易的输入vin、输出vout及锁定脚本 ,具体内容不在此讲解。
构建交易之后,需要使用$ bitcoin-cli signrawtransactionwithkey
对交易进行签名,其处理函数也在src\rpc\rawtransaction.cpp
中,主要内容是构建vin中的解锁脚本 。
签名后,就可以使用$ bitcoin-cli sendrawtransaction
将交易广播至比特币网络中,具体实现代码不在此展示。
验证交易
Bitcoin Core客户端可通过bitcoind
命令启动,启动的入口函数是src\bitcoind.cpp
中的main
函数。启动之后,Bitcoin Core会启动一个线程用于监听、接收并响应比特币网络中的信息,该线程对应的函数CConnman::ThreadMessageHandler
在src\net.cpp
中。
该函数会调用src\net_processing.cpp
中的PeerManagerImpl::ProcessMessages
函数处理接收到的信息,而它会进一步调用同在src\net_processing.cpp
中的PeerManagerImpl::ProcessMessage
函数。其被调用的关系如下:
1 | main -> AppInit -> AppInitMain -> CConnman::Start -> CConnman::ThreadMessageHandler |
在PeerManagerImpl::ProcessMessage
函数中,会根据信息类型进行不同的处理,如果是交易类型的消息,则会调用src\validation.cpp
中的AcceptToMemoryPool
函数将交易存放到交易池中。
1 | void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, CDataStream& vRecv, |
AcceptToMemoryPool
函数进而调用AcceptToMemoryPoolWithTime
函数,接着调用MemPoolAccept:: AcceptSingleTransaction
函数。
而这个MemPoolAccept::AcceptSingleTransaction
函数,是接收交易的入口函数。在这个函数中,又会调用MemPoolAccept::PreChecks
函数,它是关键中的关键,负责对交易进行全方位的检查。在这个函数中:
- 首先,会调用
src\consensus\tx_check.cpp
中的CheckTransaction
函数,进行基础的检查:
1 | bool CheckTransaction(const CTransaction& tx, TxValidationState& state) |
- 这笔被接受的交易不能是铸币交易:
1 | // Coinbase is only valid in a block, not as a loose transaction |
- 检查是否为标准交易,调用
src\policy\policy.cpp
中的IsStandardTx
函数,检查交易的版本、大小、脚本、输出中UTXO个数等是否符合标准。
1 | std::string reason; |
- 交易的字节大小不能太小。
1 | if (::GetSerializeSize(tx, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS) < MIN_STANDARD_TX_NONWITNESS_SIZE) |
- 只接受
nLockTime
满足要求、能够被打包进下一个被挖区块中的交易,防止交易过多溢出交易池。
1 | // Only accept nLockTime-using transactions that can be mined in the next |
- 检查这笔交易是否已存在交易池中,或有相同未认证的数据已在交易池中。
1 | if (m_pool.exists(GenTxid(true, tx.GetWitnessHash()))) { |
- 检查交易输入所指向的上一笔交易输出
prevout
是否与交易池中某笔交易的一样,即防止双花。
1 | // Check for conflicts with in-memory transactions |
- 检查交易所有输入的来源(UTXO)是否已在缓存中,若不在则获取。
1 | const CCoinsViewCache& coins_cache = m_active_chainstate.CoinsTip(); |
- 检查时间锁
sequence
是否满足要求,即可以被打包进下一个待挖区块中,不然就丢弃。
1 | // Only accept BIP68 sequence locked transactions that can be mined in the next |
- 调用
src\consensus\tx_verify.cpp
中的Consensus::CheckTxInputs
函数检查交易的输入。
1 | bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee) |
- 检查输入和输入的见证脚本是否符合标准(解锁脚本能否解开prevout的锁定脚本)。
1 | // Check for non-standard pay-to-script-hash in inputs |
- 交易中的签名数量(sigops)应小于签名操作数量上限。
1 | if (nSigOpsCost > MAX_STANDARD_TX_SIGOPS_COST) |
创建区块
在Bitcoin Core中,可通过$ bitcoin-cli generatetoaddress
命令进行挖矿,命令内部实现定义在src\rpc\mining.cpp
的generatetoaddress
函数中:
1 | static RPCHelpMan generatetoaddress() |
进行一系列检查后,它将调用同在src\rpc\mining.cpp
中的generateBlocks
函数,它是创建区块的入口函数:
1 | static UniValue generateBlocks(ChainstateManager& chainman, const CTxMemPool& mempool, const CScript& coinbase_script, int nGenerate, uint64_t nMaxTries) |
该函数调用了两个关键函数:
CreateNewBlock
src\miner.cpp
中的BlockAssembler::CreateNewBlock
函数,用于构造候选区块:
1 | std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn) |
GenerateBlock
src\rpc\mining.cpp
中的GenerateBlock
函数,用于挖矿:
1 | static bool GenerateBlock(ChainstateManager& chainman, CBlock& block, uint64_t& max_tries, unsigned int& extra_nonce, uint256& block_hash) |
处理区块
书接上文,创建区块之后,会调用src\validation.cpp
中的ChainstateManager::ProcessNewBlock
函数处理新区块。
同样,在接收到其它节点传播来的区块信息后,也会调用这个函数。“验证交易”一节中提到src\net_processing.cpp
中的PeerManagerImpl::ProcessMessage
函数用于处理从网络中接收到的信息,如果信息是区块类型,则会调用PeerManagerImpl::ProcessBlock
函数。
1 | void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, CDataStream& vRecv, |
而PeerManagerImpl::ProcessBlock
函数则是直接调用ChainstateManager::ProcessNewBlock
函数。
1 | void PeerManagerImpl::ProcessBlock(CNode& node, const std::shared_ptr<const CBlock>& block, bool force_processing) |
src\validation.cpp
中的ChainstateManager::ProcessNewBlock
函数是处理新区块的入口函数/关键函数,它将主要负责:验证区块、接收区块、更新最长链。
1 | bool ChainstateManager::ProcessNewBlock(const CChainParams& chainparams, const std::shared_ptr<const CBlock>& block, bool force_processing, bool* new_block) |
它主要调用如下三个函数。
CheckBlock
src\validation.cpp
中的CheckBlock
函数,负责对区块进行检查:
1 | // fCheckPOW和fCheckMerkleRoot两个参数的默认值是true |
AcceptBlock
src\validation.cpp
中的CChainState::AcceptBlock
函数,用于接收区块,负责基本的验证、链接到对应链上、广播到网络中、保存到本地磁盘等。
1 | /** Store block on disk. If dbp is non-nullptr, the file is known to already reside on disk */ |
ActivateBestChain
src\validation.cpp
中的CChainState::ActivateBestChain
函数,更新当前链为最长链。
1 | bool CChainState::ActivateBestChain(BlockValidationState& state, std::shared_ptr<const CBlock> pblock) |
选择最长链
在处理区块中所调用的CChainState::ActivateBestChain
函数,负责更新当前链为最长链。在其过程中,会调用src\validation.cpp
中的CChainState::FindMostWorkChain
函数获取最长链。
1 | /** |
FindMostWorkChain
函数所在的CChainState
类中包含一个属性:std::set<CBlockIndex*, CBlockIndexWorkComparator> setBlockIndexCandidates
候选链集合,该集合的排序规则定义如下:
1 | bool CBlockIndexWorkComparator::operator()(const CBlockIndex *pa, const CBlockIndex *pb) const { |