比特币源码分析--挖矿的实现
https://blog.csdn.net/ztemt_sw2/article/details/80958087
挖矿应该是近几年非常流行的一个名词了,通过前面文章的介绍我们现在已经知道了:在区块链中,所谓的挖矿其实是系统通过共识算法就“由谁来向区块链中写入区块并获取奖励”一事达成一致的过程。本文通过分析比特币源码,从技术角度来分析一下挖矿是如何实现的。
1 挖矿的流程
可以说,比特币就是靠挖矿来运作的,挖矿不仅保证比特币了比特币系统的安全性,同时比特币也是通过挖矿的方式来发行的,先简要概括一下挖矿流程:
当一个矿工收到一个新区块的广播以后,证明已经有人抢先一步了,矿工对收到的区块的工作量和区块中的交易校验无误后就将区块添加到区块链中,然后立刻投入到挖掘下一个区块的竞赛中:
(1) 矿工从交易池中选择一批交易构造出候选区块;
(2) 生成工作量证明(hash运算,拼算力);
(3) 算出工作量证明后立即将区块加入到本地区块链,并将区块广播到到网络中;
(4) 网络中其他的矿工节点收到区块,验证无误后也将区块加入自己的区块链,共识完成;
(5) 开始下一个挖矿周期。
2 源码分析
2.1 区块链相关的数据结构和变量
在分析源码之前,首先了解几个重要的数据结构和变量,这些数据结构和变量对于理解区块链非常之重要。
2.1.1 CBlockIndex
因为区块链有可能存在分叉的情况,所以从逻辑上讲它的数据结构是一颗树,出现分叉时,每条分叉对应树的一条分支。在内存中区块链是由CBlockIndex的向量来表示的。
-
class CBlockIndex -
{ -
public: -
//! pointer to the hash of the block, if any. Memory is owned by this CBlockIndex -
//指向区块hash值的指针 -
const uint256* phashBlock; -
//! pointer to the index of the predecessor of this block -
//指向上一个区块的指针,因为区块链可能存在分叉的情况,所以一个区块可能有多个指针指向它 -
CBlockIndex* pprev; -
//! pointer to the index of some further predecessor of this block -
// 指向 此区块更远的祖先的指针 -
CBlockIndex* pskip; -
//! height of the entry in the chain. The genesis block has height 0 -
//该区块的高度,从创世区块开始算起 -
int nHeight; -
//! Which # file this block is stored in (blk?????.dat) -
//存储本区块的数据的文件,比如第100个区块,其区块文件存储在blk100.data中 -
int nFile; -
//! Byte offset within blk?????.dat where this block's data is stored -
//区块的数据取在blk????.data中的偏移量 -
unsigned int nDataPos; -
//! Byte offset within rev?????.dat where this block's undo data is stored -
unsigned int nUndoPos; -
//! (memory only) Total amount of work (expected number of hashes) in the chain up to and including this block -
//从创始区块到本区块的累积工作量 -
arith_uint256 nChainWork; -
//! Number of transactions in this block. -
//! Note: in a potential headers-first mode, this number cannot be relied upon -
//区块中包含的交易数 -
unsigned int nTx; -
//! (memory only) Number of transactions in the chain up to and including this block. -
//! This value will be non-zero only if and only if transactions for this block and all its parents are available. -
//! Change to 64-bit type when necessary; won't happen before 2030 -
//从创始区块到本区块的所有交易的数量,只有收到区块以及它所有的父区块的交易数据时这个值才会非0 -
unsigned int nChainTx; -
//! Verification status of this block. See enum BlockStatus -
//区块的状态 -
uint32_t nStatus; -
//! block header -
//区块头 -
int32_t nVersion; //版本 -
uint256 hashMerkleRoot; //merkel树的树根 -
uint32_t nTime; //时间戳 -
//这两个变量用于计算PoW -
uint32_t nBits; //难度位 -
uint32_t nNonce; -
//! (memory only) Sequential id assigned to distinguish order in which blocks are received. -
int32_t nSequenceId; -
//! (memory only) Maximum nTime in the chain up to and including this block. -
unsigned int nTimeMax;
上面结构中的nStatus表示区块的状态,区块可能的状态定义如下:
-
enum BlockStatus: uint32_t { -
//! Unused. -
BLOCK_VALID_UNKNOWN = 0, -
//! Parsed, version ok, hash satisfies claimed PoW, 1 <= vtx count <= max, timestamp not in future -
BLOCK_VALID_HEADER = 1, -
//! All parent headers found, difficulty matches, timestamp >= median previous, checkpoint. Implies all parents -
//! are also at least TREE. -
BLOCK_VALID_TREE = 2, -
/** -
* Only first tx is coinbase, 2 <= coinbase input script length <= 100, transactions valid, no duplicate txids, -
* sigops, size, merkle root. Implies all parents are at least TREE but not necessarily TRANSACTIONS. When all -
* parent blocks also have TRANSACTIONS, CBlockIndex::nChainTx will be set. -
*/ -
BLOCK_VALID_TRANSACTIONS = 3, -
//! Outputs do not overspend inputs, no double spends, coinbase output ok, no immature coinbase spends, BIP30. -
//! Implies all parents are also at least CHAIN. -
BLOCK_VALID_CHAIN = 4, -
//! Scripts & signatures ok. Implies all parents are also at least SCRIPTS. -
BLOCK_VALID_SCRIPTS = 5, -
//! All validity bits. -
BLOCK_VALID_MASK = BLOCK_VALID_HEADER | BLOCK_VALID_TREE | BLOCK_VALID_TRANSACTIONS | -
BLOCK_VALID_CHAIN | BLOCK_VALID_SCRIPTS, -
BLOCK_HAVE_DATA = 8, //!< full block available in blk*.dat -
BLOCK_HAVE_UNDO = 16, //!< undo data available in rev*.dat -
BLOCK_HAVE_MASK = BLOCK_HAVE_DATA | BLOCK_HAVE_UNDO, -
BLOCK_FAILED_VALID = 32, //!< stage after last reached validness failed -
BLOCK_FAILED_CHILD = 64, //!< descends from failed block -
BLOCK_FAILED_MASK = BLOCK_FAILED_VALID | BLOCK_FAILED_CHILD, -
BLOCK_OPT_WITNESS = 128, //!< block data in blk*.data was received with a witness-enforcing client -
};
BLOCK_VALID_HEADER:区块头校验OK,区块hash的工作量证明符合预期,交易数以及版本号等都正确;
BLOCK_VALID_TREE:区块所有父区块的头部都已经找到了,如果当前区块的状态是BLOCK_VALID_TREE,则其所有的父区块的状态至少也是BLOCK_VALID_TREE;
BLOCK_VALID_TRANSACTIONS:区块有有效的交易,即有且仅有一笔coinbase交易,coinbase交易是区块中的第一笔交易。如果当前区块的状态是BLOCK_VALID_TRANSCATIONS,那么它的所有父区块的状态至少是BLOCK_VALID_TREE(已经收到当前区块的交易数据,但是父区块的交易数据可能尚未收到);
BLOCK_VALID_CHAIN:交易的输出没问题,没有双花等问题。如果当前区块的状态是BLOCK_VALID_CHAIN,则它所有的父区块的状态至少也是BLOCK_VALID_CHAIN;
BLOCK_VALID_SCRIPTS:交易脚本和签名没问题。如果当前区块的状态是BLOCK_VALID_SCRIPTS,它所有父区块的状态至少也是BLOCK_VALID_SCRIPTS。
2.1.2 最长链chainActive
这个就是所谓的区块链了,chainActive是一个全局变量,代表的是当前有最大累计工作量的分支。这个全局变量在节点启动的时候从levelDB数据库中加载,后续在介绍区块链的加载和同步的时候会详细说明。
chainActive的类型如下:
-
/** An in-memory indexed chain of blocks. */ -
class CChain { -
private: -
std::vector<CBlockIndex*> vChain; -
public: -
/** Returns the index entry for the genesis block of this chain, or nullptr if none. */ -
CBlockIndex *Genesis() const { -
return vChain.size() > 0 ? vChain[0] : nullptr; -
} -
/** Returns the index entry for the tip of this chain, or nullptr if none. */ -
CBlockIndex *Tip() const { -
return vChain.size() > 0 ? vChain[vChain.size() - 1] : nullptr; -
} -
/** Returns the index entry at a particular height in this chain, or nullptr if no such height exists. */ -
CBlockIndex *operator[](int nHeight) const { -
if (nHeight < 0 || nHeight >= (int)vChain.size()) -
return nullptr; -
return vChain[nHeight]; -
} -
/** Compare two chains efficiently. */ -
friend bool operator==(const CChain &a, const CChain &b) { -
return a.vChain.size() == b.vChain.size() && -
a.vChain[a.vChain.size() - 1] == b.vChain[b.vChain.size() - 1]; -
} -
/** Efficiently check whether a block is present in this chain. */ -
bool Contains(const CBlockIndex *pindex) const { -
return (*this)[pindex->nHeight] == pindex; -
} -
/** Find the successor of a block in this chain, or nullptr if the given index is not found or is the tip. */ -
CBlockIndex *Next(const CBlockIndex *pindex) const { -
if (Contains(pindex)) -
return (*this)[pindex->nHeight + 1]; -
else -
return nullptr; -
} -
/** Return the maximal height in the chain. Is equal to chain.Tip() ? chain.Tip()->nHeight : -1. */ -
int Height() const { -
return vChain.size() - 1; -
} -
/** Set/initialize a chain with a given tip. */ -
void SetTip(CBlockIndex *pindex); -
/** Return a CBlockLocator that refers to a block in this chain (by default the tip). */ -
CBlockLocator GetLocator(const CBlockIndex *pindex = nullptr) const; -
/** Find the last common block between this chain and a block index entry. */ -
const CBlockIndex *FindFork(const CBlockIndex *pindex) const; -
/** Find the earliest block with timestamp equal or greater than the given. */ -
CBlockIndex* FindEarliestAtLeast(int64_t nTime) const; -
}; -
#endif // BITCOIN_CHAIN_H
可以看到,CChain其实是将一节介绍的CBlockIndex封装在了一个向量当中,向量中的区块按高度排序(也就是说元素的索引就是对应区块的高度),重写了[]运算符,方便检索到任意高度的区块。
2.1.3 CBlock
代表一个区块,继承自CBlockHeader,CBlockHeader包含区块头的数据。
-
class CBlock : public CBlockHeader -
{ -
public: -
// network and disk -
std::vector<CTransactionRef> vtx; -
// memory only -
mutable bool fChecked; -
CBlock() -
{ -
SetNull(); -
} -
CBlock(const CBlockHeader &header) -
{ -
SetNull(); -
*(static_cast<CBlockHeader*>(this)) = header; -
} -
ADD_SERIALIZE_METHODS; -
template <typename Stream, typename Operation> -
inline void SerializationOp(Stream& s, Operation ser_action) { -
READWRITEAS(CBlockHeader, *this); -
READWRITE(vtx); -
} -
void SetNull() -
{ -
CBlockHeader::SetNull(); -
vtx.clear(); -
fChecked = false; -
} -
CBlockHeader GetBlockHeader() const -
{ -
CBlockHeader block; -
block.nVersion = nVersion; -
block.hashPrevBlock = hashPrevBlock; -
block.hashMerkleRoot = hashMerkleRoot; -
block.nTime = nTime; -
block.nBits = nBits; -
block.nNonce = nNonce; -
return block; -
} -
std::string ToString() const; -
};
结合代码对比之前介绍的CBlockIndex,可以发现二者主要的异同在于:CBlock是纯数据(区块的交易数据和区块头),而CBlockIndex则包含区块链相关的信息(例如指向父区块的指针,在区块链上的高度等等)。
2.1.4 mapBlockIndex
这是一个以区块的hash为键,以对应区块的CBlockIndex为值的一个映射表。包含了所有区块的信息(当区块链有分叉的时候,不光是最长链,所有分叉上的区块信息也都会在这个映射表当中)。
这个映射表是在节点启动的时候从levelDB数据库中加载到内存,之后只要从网络上收到一个新区快,就会添加一项到这个表中,因此这个表只会不断增大,不会变小。
2.1.5 mapBlocksUnlinked
这个映射表的作用是在收到一个丢失的中间区块时,能快速的将区块链接到区块链上面。
其映射关系A->B表示区块A的交易数据还没收到,但是区块B的数据已经收到了。
例1:
(1) 假设当前区块链的顶点(最后一个区块)为A;
(2) 一段时间之后收到了区块B和C的区块头;
(3) 又过了一段时间后收到了区块C的完整数据,这时候mapBlocksUnlinked中就会新加一项:B->C;
(4) 一段时间后B的完整数据也收到了,此时将B->C从表中删除,然后区块B就可以连接到区块链上成为新顶点了;
例2:
(1) 设A为区块链的顶点;
(2) 收到了区块B,C,D的区块头;
(3) 紧接着收到了区块D的完整数据;
(4) 此时表中添加B->C,B->D,C->D的表项;
(5) 收到了区块B的完整数据,此时将B->C,B->D删除,将B连接到区块链上成为新的顶点,表中只剩C->D的表项。
2.1.6 setBlockIndexCandidates
这是一个集合,集合中包含了所有比区块链当前顶点工作量更多的区块的集合,此集合中包含的区块就是延长当前区块链的候选区块。之所以说这些区块时候选区块,是因为只是在收到区块头以后对区块的工作量证明做了校验,区块是否能用来延长区块链,需要等收到区块的完整数据以后才能确定。假如当前区块链的顶点是100,而我们收到了103号区块的区块头,那么还得等待并且校验中间丢失的101和102号区块。
例子:
假设当前区块链的顶点是A,然后依次收到了区块B,C,D的区块头:
A -- B
\
C ---- D
B,C,D的区块头校验没有问题,此时setBlockIndexCandidates中将包含B,C,D三个元素,假设区块B的工作量大于区块C但是小于区块D;
假设我们之后收到了区块B的完整数据,此时将B做为区块链的新的顶点,并且将B从集合中删除,同时也要将C移除,因为C的工作量小于B。
接着又收到区块C和区块D的完整数据,但是区块D的数据中包含非法交易,则区块C依然保留在上文介绍的mapBlocksIndex映射表中并保存在磁盘上,而D从mapBlocksIndex中移除,磁盘上也不保存,同时D也需要从setBlockIndexCandidates中删除。
2.1.7 pindexBestHeader
这是一个全局变量,指向当前具有最大工作量的区块。在节点启动从db加载区块链时初始化,每当收到一个工作量更大的区块时更新。
注意这个变量和chainActive全局变量的tip()方法的区别:
chainActive.tip返回的是当前区块链的顶点,这个顶点是在收到完整区块数据并且校验无误后加到区块链上的,而pindexBestHeader指向的是节点收到的具有最大工作量的区块,该区块可能是一个只有区块头的不完整区块。
比如当前区块链的顶点(chainActive.tip())是100,然后节点可能收到了103号区块的区块头,则pindexBestHeader指向的就是103好区块。
2.2 构造候选区块
上一节介绍了和区块链相关的一些重要的数据结构和变量,这些在后续的学习中都需要用到,因此请先记住他们。
接下来正式踏上挖矿之旅,首先从构造区块开始。当节点收到一个新区块的通知后,意味着节点在这一轮的挖矿竞争中失败了。此时节点对收到的新区块进行验证,包括工作量是否满足,是否有非法交易等等,验证无误后节点将区块添加到区块链,并立刻开始构造候选区块,开始新一轮的挖矿竞赛。
可以通过bitcoin-cli generatetoaddress命令挖取新区块,我们看看这个命令是如何实现的。
-
static UniValue generatetoaddress(const JSONRPCRequest& request) -
{ -
if (request.fHelp || request.params.size() < 2 || request.params.size() > 3) -
throw std::runtime_error( -
"generatetoaddress nblocks address (maxtries)\n" -
"\nMine blocks immediately to a specified address (before the RPC call returns)\n" -
"\nArguments:\n" -
"1. nblocks (numeric, required) How many blocks are generated immediately.\n" -
"2. address (string, required) The address to send the newly generated bitcoin to.\n" -
"3. maxtries (numeric, optional) How many iterations to try (default = 1000000).\n" -
"\nResult:\n" -
"[ blockhashes ] (array) hashes of blocks generated\n" -
"\nExamples:\n" -
"\nGenerate 11 blocks to myaddress\n" -
+ HelpExampleCli("generatetoaddress", "11 \"myaddress\"") -
); -
int nGenerate = request.params[0].get_int(); -
uint64_t nMaxTries = 1000000; -
if (!request.params[2].isNull()) { -
nMaxTries = request.params[2].get_int(); -
} -
CTxDestination destination = DecodeDestination(request.params[1].get_str()); -
if (!IsValidDestination(destination)) { -
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Error: Invalid address"); -
} -
std::shared_ptr<CReserveScript> coinbaseScript = std::make_shared<CReserveScript>(); -
coinbaseScript->reserveScript = GetScriptForDestination(destination); -
return generateBlocks(coinbaseScript, nGenerate, nMaxTries, false); //这里开始生成新区块 -
}
代码的最后,通过generateBlocks来生成新区块。继续跟进去:
-
UniValue generateBlocks(std::shared_ptr<CReserveScript> coinbaseScript, int nGenerate, uint64_t nMaxTries, bool keepScript) -
{ -
static const int nInnerLoopCount = 0x10000; -
int nHeightEnd = 0; -
int nHeight = 0; -
{ // Don't keep cs_main locked -
LOCK(cs_main); -
//当前区块链的高度 -
nHeight = chainActive.Height(); -
//挖矿后的目标高度,例如当前高度100,总共挖3个区块,则目标高度就是103 -
nHeightEnd = nHeight+nGenerate; -
} -
unsigned int nExtraNonce = 0; -
UniValue blockHashes(UniValue::VARR); -
//开始挖矿 -
while (nHeight < nHeightEnd && !ShutdownRequested()) -
{ -
//构造候选区块 -
std::unique_ptr<CBlockTemplate> pblocktemplate(BlockAssembler(Params()).CreateNewBlock(coinbaseScript->reserveScript)); -
if (!pblocktemplate.get()) -
throw JSONRPCError(RPC_INTERNAL_ERROR, "Couldn't create new block"); -
CBlock *pblock = &pblocktemplate->block; -
{ -
LOCK(cs_main); -
IncrementExtraNonce(pblock, chainActive.Tip(), nExtraNonce); -
} -
//计算PoW工作量证明,拼算力的地方就在这里 -
while (nMaxTries > 0 && pblock->nNonce < nInnerLoopCount && !CheckProofOfWork(pblock->GetHash(), pblock->nBits, Params().GetConsensus())) { -
++pblock->nNonce; -
--nMaxTries; -
} -
if (nMaxTries == 0) { -
break; -
} -
//超过单轮循环的上限(65536次),构造候选区块重新开始 -
if (pblock->nNonce == nInnerLoopCount) { -
continue; -
} -
//处理新区块 -
std::shared_ptr<const CBlock> shared_pblock = std::make_shared<const CBlock>(*pblock); -
if (!ProcessNewBlock(Params(), shared_pblock, true, nullptr)) -
throw JSONRPCError(RPC_INTERNAL_ERROR, "ProcessNewBlock, block not accepted"); -
++nHeight; -
blockHashes.push_back(pblock->GetHash().GetHex()); -
//mark script as important because it was used at least for one coinbase output if the script came from the wallet -
if (keepScript) -
{ -
coinbaseScript->KeepScript(); -
} -
} -
return blockHashes; -
}
其中构造新区块是通过BlockAssembler.CreateNewBlock来做的:
-
std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn, bool fMineWitnessTx) -
{ -
int64_t nTimeStart = GetTimeMicros(); -
resetBlock(); -
pblocktemplate.reset(new CBlockTemplate()); -
if(!pblocktemplate.get()) -
return nullptr; -
pblock = &pblocktemplate->block; // pointer for convenience -
// Add dummy coinbase tx as first transaction -
//添加一笔伪交易作为coinbase交易,一个区块中的第一笔交易必须是coinbase交易,第一个位置留给coinbase交易 -
pblock->vtx.emplace_back(); -
pblocktemplate->vTxFees.push_back(-1); // updated at end -
pblocktemplate->vTxSigOpsCost.push_back(-1); // updated at end -
LOCK2(cs_main, mempool.cs); -
//拿到当前区块链的顶点,作为新区块的父区块 -
CBlockIndex* pindexPrev = chainActive.Tip(); -
assert(pindexPrev != nullptr); -
//新区块的高度是当前区块链顶点高度+1 -
nHeight = pindexPrev->nHeight + 1; -
//计算区块版本 -
pblock->nVersion = ComputeBlockVersion(pindexPrev, chainparams.GetConsensus()); -
// -regtest only: allow overriding block.nVersion with -
// -blockversion=N to test forking scenarios -
if (chainparams.MineBlocksOnDemand()) -
pblock->nVersion = gArgs.GetArg("-blockversion", pblock->nVersion); -
//计算区块的时间戳,时间戳是节点当前时间减去一个偏移值得到 -
pblock->nTime = GetAdjustedTime(); -
const int64_t nMedianTimePast = pindexPrev->GetMedianTimePast(); -
nLockTimeCutoff = (STANDARD_LOCKTIME_VERIFY_FLAGS & LOCKTIME_MEDIAN_TIME_PAST) -
? nMedianTimePast -
: pblock->GetBlockTime(); -
// Decide whether to include witness transactions -
// This is only needed in case the witness softfork activation is reverted -
// (which would require a very deep reorganization) or when -
// -promiscuousmempoolflags is used. -
// TODO: replace this with a call to main to assess validity of a mempool -
// transaction (which in most cases can be a no-op). -
fIncludeWitness = IsWitnessEnabled(pindexPrev, chainparams.GetConsensus()) && fMineWitnessTx; -
int nPackagesSelected = 0; -
int nDescendantsUpdated = 0; -
//从交易池中选择一批交易打包到区块中(注意:并不会从交易持中将交易删除,删除需要等区块确认以后) -
addPackageTxs(nPackagesSelected, nDescendantsUpdated); -
int64_t nTime1 = GetTimeMicros(); -
nLastBlockTx = nBlockTx; -
nLastBlockWeight = nBlockWeight; -
// Create coinbase transaction. -
//创建coinbase交易 -
CMutableTransaction coinbaseTx; -
coinbaseTx.vin.resize(1); -
coinbaseTx.vin[0].prevout.SetNull(); -
coinbaseTx.vout.resize(1); -
coinbaseTx.vout[0].scriptPubKey = scriptPubKeyIn; -
//coinbase交易的输出实际上就是给矿工的奖励:所有交易费用之和+系统发放的比特币奖励 -
coinbaseTx.vout[0].nValue = nFees + GetBlockSubsidy(nHeight, chainparams.GetConsensus()); -
coinbaseTx.vin[0].scriptSig = CScript() << nHeight << OP_0; -
pblock->vtx[0] = MakeTransactionRef(std::move(coinbaseTx)); -
pblocktemplate->vchCoinbaseCommitment = GenerateCoinbaseCommitment(*pblock, pindexPrev, chainparams.GetConsensus()); -
pblocktemplate->vTxFees[0] = -nFees; -
LogPrintf("CreateNewBlock(): block weight: %u txs: %u fees: %ld sigops %d\n", GetBlockWeight(*pblock), nBlockTx, nFees, nBlockSigOpsCost); -
// Fill in header -
//填充父区块的区块hash -
pblock->hashPrevBlock = pindexPrev->GetBlockHash(); -
UpdateTime(pblock, chainparams.GetConsensus(), pindexPrev); -
//设置新区块的工作量难度目标值 -
pblock->nBits = GetNextWorkRequired(pindexPrev, pblock, chainparams.GetConsensus()); -
//nonce,计算工作量证明的时候就是把这个值不断递增来算hash -
pblock->nNonce = 0; -
pblocktemplate->vTxSigOpsCost[0] = WITNESS_SCALE_FACTOR * GetLegacySigOpCount(*pblock->vtx[0]); -
CValidationState state; -
if (!TestBlockValidity(state, chainparams, *pblock, pindexPrev, false, false)) { -
throw std::runtime_error(strprintf("%s: TestBlockValidity failed: %s", __func__, FormatStateMessage(state))); -
} -
int64_t nTime2 = GetTimeMicros(); -
LogPrint(BCLog::BENCH, "CreateNewBlock() packages: %.2fms (%d packages, %d updated descendants), validity: %.2fms (total %.2fms)\n", 0.001 * (nTime1 - nTimeStart), nPackagesSelected, nDescendantsUpdated, 0.001 * (nTime2 - nTime1), 0.001 * (nTime2 - nTimeStart)); -
return std::move(pblocktemplate); -
}
关键的地方已经做了注释,结合代码很容易理解新区块的构造过程。
2.2.1 矿工奖励费的计算
上面的代码中我们看到一个新区块的第一笔交易必须是coinbase交易,coibase交易是一种特殊的交易,用来奖励挖矿的矿工。奖励费的计算方法代码中已经看到了:
coinbaseTx.vout[0].nValue = nFees + GetBlockSubsidy(nHeight, chainparams.GetConsensus());
奖励费=区块中所有交易的交易费 + 系统奖励的比特币
其中交易费是在从交易池中选择交易的时候,按交易费的多少选出的交易计算一个总和(矿工肯定会优先选择交易费多的交易进入区块,谁也不会跟钱过不去),而系统奖励的比特币则是随着一定的时间来递减,具体算法参考下面代码:
-
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; -
}
算法需要根据当前区块的高度值来确定给矿工的奖励费。
首先用当前区块的高度除以nSubsidvHalvingInterval,这个值为210000(在chainparams.cpp中定义),如果相除的结果大于或等于64,则奖励费为0。换句话说,当区块的高度超过64 * 210000的时候,系统将不会在有奖励费。按比特币平均每10分钟生成一个区块算,生成第64 * 210000个区块需要大约255年的时间,比特币诞生于2009年,意味着大约到2214年左右,挖矿只能得到交易费,不再有系统的比特币奖励产生(比特币用完了)。
如果区块的高度小于64 * 210000,则初始时(从比特币诞生开始算起)系统奖励是50个比特币,然后每隔21000个区块奖励费减半,按比特币平均10分钟一个区块算,210000个区块需要3.99年(约4年的时间),也就是代码注释中所说的平均每隔4年奖励费减半。
2.2.2 工作量难度的设定
新区块的区块头中包含了新区块的工作量计算的难度目标值:
pblock->nBits = GetNextWorkRequired(pindexPrev, pblock, chainparams.GetConsensus());
我们看看这个目标工作量是如何设定出来的:
-
unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHeader *pblock, const Consensus::Params& params) -
{ -
assert(pindexLast != nullptr); -
//极限难度值 -
unsigned int nProofOfWorkLimit = UintToArith256(params.powLimit).GetCompact(); -
// Only change once per difficulty adjustment interval -
//距离上一次难度调整是否过了2016个区块,如果没有就沿用用当前区块链顶点区块的难度值 -
if ((pindexLast->nHeight+1) % params.DifficultyAdjustmentInterval() != 0) -
{ -
if (params.fPowAllowMinDifficultyBlocks) -
{ -
// Special difficulty rule for testnet: -
// If the new block's timestamp is more than 2* 10 minutes -
// then allow mining of a min-difficulty block. -
if (pblock->GetBlockTime() > pindexLast->GetBlockTime() + params.nPowTargetSpacing*2) -
return nProofOfWorkLimit; -
else -
{ -
// Return the last non-special-min-difficulty-rules-block -
const CBlockIndex* pindex = pindexLast; -
while (pindex->pprev && pindex->nHeight % params.DifficultyAdjustmentInterval() != 0 && pindex->nBits == nProofOfWorkLimit) -
pindex = pindex->pprev; -
return pindex->nBits; -
} -
} -
return pindexLast->nBits; -
} -
// Go back by what we want to be 14 days worth of blocks -
//超过了2016个区块,需要重新调整一次难度 -
int nHeightFirst = pindexLast->nHeight - (params.DifficultyAdjustmentInterval()-1); -
assert(nHeightFirst >= 0); -
const CBlockIndex* pindexFirst = pindexLast->GetAncestor(nHeightFirst); -
assert(pindexFirst); -
return CalculateNextWorkRequired(pindexLast, pindexFirst->GetBlockTime(), params); -
}
2.2.2.1 目标难度值的自动调整
首先系统是以2016个区块为一个周期来自动进行难度调整的,这个周期值根据如下方法来确定:
int64_t DifficultyAdjustmentInterval() const { return nPowTargetTimespan / nPowTargetSpacing; }
其中nPowTargetTimeSpan和nPowTargetSpacing的定义位于chainparams.cpp中:
-
consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks -
consensus.nPowTargetSpacing = 10 * 60;
nPowTargetTimeSpan/nPowTargetSpacing=2016。
如果从上一次难度调整开始算起,新生成了2016个区块,则进行一次难度调整,调整的方法如下:
-
unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, int64_t nFirstBlockTime, const Consensus::Params& params) -
{ -
if (params.fPowNoRetargeting) -
return pindexLast->nBits; -
// Limit adjustment step -
//计算最近的2016个区块实际花费了多长时间 -
int64_t nActualTimespan = pindexLast->GetBlockTime() - nFirstBlockTime; -
//将实际时间限制在3.5天-8周内,低于3.5天的按3.5天算,高于8周的按8周算 -
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(); -
}
设新的难度值为T,旧的难度值为O,最近2016个区块实际耗费时间为Dur,系统期望的生成2016个区块的时间是20160分钟,也就是每10分钟产生一个区块,则根据代码:
变换一下上面的公式:
假如Dur小于20160,也就是最近2016个区块的实际时间小于期望值(区块出的太快了),则目标难度Target变小,从而求解难度变大(注意这里是反比关系,值越小难度越大);反过来,如果最近2016个区块的实际时间大于20160(区块出的太慢了),则Target变大,从而求解难度变小。
通过这种定期自动调整难度的方式,比特币系统保证了平均每10分钟生成一个区块。
2.2.2.2 难度值的表示
区块头的难度目标值nBit是一个32位(4字节)的无符号整数,这个无符号整数的最高一个字节代表幂,假设为P,后三个字节为系数,假设为Factor,则目标难度值T最终表示为:
因为是指数级,可以预想到最终的难度值是非常大的,比如比特币区块链中的第277316号区块,nBit的值转换为16进制为0x1903a30c,按照上面的公式,目标难度T:
最终得到的值为22829202948393929850749706076701368331072452018388575715328,将这个值转换为16进制后就是最终hash计算的目标值:
0x0000000000000003A30C00000000000000000000000000000000000000000000。
由nBit生成最终难度值的代码如下:
-
arith_uint256& arith_uint256::SetCompact(uint32_t nCompact, bool* pfNegative, bool* pfOverflow) -
{ -
//nBit右移3个字节,得到幂p -
int nSize = nCompact >> 24; -
//计算系数 -
uint32_t nWord = nCompact & 0x007fffff; -
//幂p<=3的需要调整一下,否则幂就是8 * (p-3) -
if (nSize <= 3) { -
nWord >>= 8 * (3 - nSize); -
*this = nWord; -
} else { -
*this = nWord; -
*this <<= 8 * (nSize - 3); -
} -
if (pfNegative) -
*pfNegative = nWord != 0 && (nCompact & 0x00800000) != 0; -
if (pfOverflow) -
*pfOverflow = nWord != 0 && ((nSize > 34) || -
(nWord > 0xff && nSize > 33) || -
(nWord > 0xffff && nSize > 32)); -
return *this; -
}
2.3 计算工作量
经过上一节的介绍,我们已经知道了一个新区块是如何构造的,新的区块的目标工作量也已经确定并保存在区块的nBit域当中,接下来就是暴力**,比拼算力的时候了,回头再来看看代码:
-
while (nHeight < nHeightEnd && !ShutdownRequested()) -
{ -
//构造候选区块 -
std::unique_ptr<CBlockTemplate> pblocktemplate(BlockAssembler(Params()).CreateNewBlock(coinbaseScript->reserveScript)); -
if (!pblocktemplate.get()) -
throw JSONRPCError(RPC_INTERNAL_ERROR, "Couldn't create new block"); -
CBlock *pblock = &pblocktemplate->block; -
{ -
LOCK(cs_main); -
IncrementExtraNonce(pblock, chainActive.Tip(), nExtraNonce); -
} -
//计算PoW工作量证明,拼算力的地方就在这里 -
while (nMaxTries > 0 && pblock->nNonce < nInnerLoopCount && !CheckProofOfWork(pblock->GetHash(), pblock->nBits, Params().GetConsensus())) { -
++pblock->nNonce; -
--nMaxTries; -
} -
if (nMaxTries == 0) { -
break; -
} -
//超过单轮循环的上限(65536次),构造候选区块重新开始 -
if (pblock->nNonce == nInnerLoopCount) { -
continue; -
}
系统以nInnerLoopCount(65536)为一个周期计算工作量,计算的方法:
-
bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params& params) -
{ -
bool fNegative; -
bool fOverflow; -
arith_uint256 bnTarget; -
bnTarget.SetCompact(nBits, &fNegative, &fOverflow); //将nBits按公式生成最终的难度值 -
// Check range -
if (fNegative || bnTarget == 0 || fOverflow || bnTarget > UintToArith256(params.powLimit)) -
return false; -
// Check proof of work matches claimed amount -
if (UintToArith256(hash) > bnTarget) -
return false; -
return true; -
}
计算的方法很简单:拿到当前区块的hash,比较是否大于目标难度值,如果小于等于目标难度值则完成工作,否则需要把区块的nonce值+1然后再次计算区块头hash,如此循环下去直到得到满足条件的区块hash为止。
2.4 处理新区快
一旦计算出了工作量满足条件的新区块,接下来就要处理新区块,把新区块添加到本地区块链上,并且将新区块通过P2P网络广播出去,让其他节点验证最终达成共识,将新区块加入到节点本地的区块链上(网络节点达成共识:没错,你的这个区块没问题,我们承认你应该拿到比特币奖励)。
再次回头看之前的代码:
-
while (nMaxTries > 0 && pblock->nNonce < nInnerLoopCount && !CheckProofOfWork(pblock->GetHash(), pblock->nBits, Params().GetConsensus())) { -
++pblock->nNonce; -
--nMaxTries; -
} -
if (nMaxTries == 0) { -
break; -
} -
//超过单轮循环的上限(65536次),构造候选区块重新开始 -
if (pblock->nNonce == nInnerLoopCount) { -
continue; -
} -
//处理新区块 -
std::shared_ptr<const CBlock> shared_pblock = std::make_shared<const CBlock>(*pblock); -
if (!ProcessNewBlock(Params(), shared_pblock, true, nullptr)) -
throw JSONRPCError(RPC_INTERNAL_ERROR, "ProcessNewBlock, block not accepted"); -
++nHeight; -
blockHashes.push_back(pblock->GetHash().GetHex());
一旦成功计算出满足条件的工作量,将跳出循环,然后执行ProcessNewBlock来处理新区块,如果处理失败,抛出异常告知新区块没有被接受,否则将新区块的hash值保存起来。
ProcessNewBlock这个函数非常重要,来看看其流程:
-
bool ProcessNewBlock(const CChainParams& chainparams, const std::shared_ptr<const CBlock> pblock, bool fForceProcessing, bool *fNewBlock) -
{ -
AssertLockNotHeld(cs_main); -
{ -
CBlockIndex *pindex = nullptr; -
if (fNewBlock) *fNewBlock = false; -
CValidationState state; -
// Ensure that CheckBlock() passes before calling AcceptBlock, as -
// belt-and-suspenders. -
//先对区块进行检查 -
bool ret = CheckBlock(*pblock, state, chainparams.GetConsensus()); -
LOCK(cs_main); -
if (ret) { -
// Store to disk -
//一系列的操作,确认区块是否合法,将区块存入磁盘 -
ret = g_chainstate.AcceptBlock(pblock, state, chainparams, &pindex, fForceProcessing, nullptr, fNewBlock); -
} -
if (!ret) { -
GetMainSignals().BlockChecked(*pblock, state); -
return error("%s: AcceptBlock FAILED (%s)", __func__, FormatStateMessage(state)); -
} -
} -
//通知UI层 -
NotifyHeaderTip(); -
CValidationState state; // Only used to report errors, not invalidity - ignore it -
//将新区块加入到本地区块链,延长本地最长(具有最大工作量)链 -
if (!g_chainstate.ActivateBestChain(state, chainparams, pblock)) -
return error("%s: ActivateBestChain failed (%s)", __func__, FormatStateMessage(state)); -
return true; -
}
整体上看似乎流程很简单的三步走:检查区块->区块数据存入磁盘->将新区块加入区块链,但是展开以后,故事情节其实还是蛮复杂,需要一点一点消化。
2.4.1 检查区块
新构造的区块要想加入到区块链,要过的第一关就是基本的区块检查,包括区块的工作量是否满足,第一笔交易是否是coinbase交易,是否只有一笔coinbase交易,交易是否正常等等:
-
bool CheckBlock(const CBlock& block, CValidationState& state, const Consensus::Params& consensusParams, bool fCheckPOW, bool fCheckMerkleRoot) -
{ -
// These are checks that are independent of context. -
if (block.fChecked) -
return true; -
// Check that the header is valid (particularly PoW). This is mostly -
// redundant with the call in AcceptBlockHeader. -
//检查区块的工作量是否OK -
if (!CheckBlockHeader(block, state, consensusParams, fCheckPOW)) -
return false; -
// Check the merkle root. -
//检查区块的merkel树的hash值是否一致(确保交易没有被篡改过) -
if (fCheckMerkleRoot) { -
bool mutated; -
uint256 hashMerkleRoot2 = BlockMerkleRoot(block, &mutated); -
if (block.hashMerkleRoot != hashMerkleRoot2) -
return state.DoS(100, false, REJECT_INVALID, "bad-txnmrklroot", true, "hashMerkleRoot mismatch"); -
// Check for merkle tree malleability (CVE-2012-2459): repeating sequences -
// of transactions in a block without affecting the merkle root of a block, -
// while still invalidating it. -
if (mutated) -
return state.DoS(100, false, REJECT_INVALID, "bad-txns-duplicate", true, "duplicate transaction"); -
} -
// All potential-corruption validation must be done before we do any -
// transaction validation, as otherwise we may mark the header as invalid -
// because we receive the wrong transactions for it. -
// Note that witness malleability is checked in ContextualCheckBlock, so no -
// checks that use witness data may be performed here. -
// Size limits -
//区块中交易的数量是否满足条件 -
if (block.vtx.empty() || block.vtx.size() * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT || ::GetSerializeSize(block, SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS) * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT) -
return state.DoS(100, false, REJECT_INVALID, "bad-blk-length", false, "size limits failed"); -
// First transaction must be coinbase, the rest must not be -
//第一笔交易必须是coinbase交易,并且只能有一笔coinbase交易 -
if (block.vtx.empty() || !block.vtx[0]->IsCoinBase()) -
return state.DoS(100, false, REJECT_INVALID, "bad-cb-missing", false, "first tx is not coinbase"); -
for (unsigned int i = 1; i < block.vtx.size(); i++) -
if (block.vtx[i]->IsCoinBase()) -
return state.DoS(100, false, REJECT_INVALID, "bad-cb-multiple", false, "more than one coinbase"); -
// Check transactions -
//检查每一笔交易 -
for (const auto& tx : block.vtx) -
if (!CheckTransaction(*tx, state, false)) -
return state.Invalid(false, state.GetRejectCode(), state.GetRejectReason(), -
strprintf("Transaction check failed (tx hash %s) %s", tx->GetHash().ToString(), state.GetDebugMessage())); -
unsigned int nSigOps = 0; -
for (const auto& tx : block.vtx) -
{ -
nSigOps += GetLegacySigOpCount(*tx); -
} -
if (nSigOps * WITNESS_SCALE_FACTOR > MAX_BLOCK_SIGOPS_COST) -
return state.DoS(100, false, REJECT_INVALID, "bad-blk-sigops", false, "out-of-bounds SigOpCount"); -
if (fCheckPOW && fCheckMerkleRoot) -
block.fChecked = true; -
return true; -
}
有长长的一串检查清单,代码中已经做了注释,应该非常容易理解。
2.4.2 区块存盘
对新区块进行检查无误后,接下来需要将区块数据保存到磁盘上,这一步通过CChainState::AcceptBlock来完成,整个过程较为复杂,先来看看整体的处理过程:
-
bool CChainState::AcceptBlock(const std::shared_ptr<const CBlock>& pblock, CValidationState& state, const CChainParams& chainparams, CBlockIndex** ppindex, bool fRequested, const CDiskBlockPos* dbp, bool* fNewBlock) -
{ -
const CBlock& block = *pblock; -
if (fNewBlock) *fNewBlock = false; -
AssertLockHeld(cs_main); -
CBlockIndex *pindexDummy = nullptr; -
CBlockIndex *&pindex = ppindex ? *ppindex : pindexDummy; -
//校验区块头,找到区块的区块索引(CBlockIndex),没有就创建一个并加入到mapBlocksIndex映射表中 -
if (!AcceptBlockHeader(block, state, chainparams, &pindex)) -
return false; -
// Try to process all requested blocks that we don't have, but only -
// process an unrequested block if it's new and has enough work to -
// advance our tip, and isn't too many blocks ahead. -
//区块的数据是否已经在磁盘上 -
bool fAlreadyHave = pindex->nStatus & BLOCK_HAVE_DATA; -
//新区快是否有更多的工作量 -
bool fHasMoreOrSameWork = (chainActive.Tip() ? pindex->nChainWork >= chainActive.Tip()->nChainWork : true); -
// Blocks that are too out-of-order needlessly limit the effectiveness of -
// pruning, because pruning will not delete block files that contain any -
// blocks which are too close in height to the tip. Apply this test -
// regardless of whether pruning is enabled; it should generally be safe to -
// not process unrequested blocks. -
//是否是一个相隔当前区块链顶点区块太远的区块 -
bool fTooFarAhead = (pindex->nHeight > int(chainActive.Height() + MIN_BLOCKS_TO_KEEP)); -
// TODO: Decouple this function from the block download logic by removing fRequested -
// This requires some new chain data structure to efficiently look up if a -
// block is in a chain leading to a candidate for best tip, despite not -
// being such a candidate itself. -
// TODO: deal better with return value and error conditions for duplicate -
// and unrequested blocks. -
//如果区块数据已经在磁盘上了,忽略随后的操作直接返回 -
if (fAlreadyHave) return true; -
//如果不是从网络收到的新区块(新构造的区块) -
if (!fRequested) { // If we didn't ask for it: -
//区块之前已经处理过,忽略 -
if (pindex->nTx != 0) return true; // This is a previously-processed block that was pruned -
//工作量不够,忽略 -
if (!fHasMoreOrSameWork) return true; // Don't process less-work chains -
//太远的区块,忽略 -
if (fTooFarAhead) return true; // Block height is too high -
// Protect against DoS attacks from low-work chains. -
// If our tip is behind, a peer could try to send us -
// low-work blocks on a fake chain that we would never -
// request; don't process these. -
if (pindex->nChainWork < nMinimumChainWork) return true; -
} -
if (fNewBlock) *fNewBlock = true; -
//检查区块,如果区块有问题,将区块的状态标记为无效(BLOCK_FAILED_VALID),并且加入到脏区块集合中 -
if (!CheckBlock(block, state, chainparams.GetConsensus()) || -
!ContextualCheckBlock(block, state, chainparams.GetConsensus(), pindex->pprev)) { -
if (state.IsInvalid() && !state.CorruptionPossible()) { -
pindex->nStatus |= BLOCK_FAILED_VALID; -
setDirtyBlockIndex.insert(pindex); -
} -
return error("%s: %s", __func__, FormatStateMessage(state)); -
} -
// Header is valid/has work, merkle tree and segwit merkle tree are good...RELAY NOW -
// (but if it does not build on our best tip, let the SendMessages loop relay it) -
if (!IsInitialBlockDownload() && chainActive.Tip() == pindex->pprev) -
GetMainSignals().NewPoWValidBlock(pindex, pblock);//区块头广播到网络中 -
// Write block to history file -
try { -
//将区块存储到磁盘上 -
CDiskBlockPos blockPos = SaveBlockToDisk(block, pindex->nHeight, chainparams, dbp); -
if (blockPos.IsNull()) { -
state.Error(strprintf("%s: Failed to find position to write new block to disk", __func__)); -
return false; -
} -
//标记区块的数据已经接收并且检查过了(区块的状态更新为BLOCK_VALID_TRANSACTIONS) -
if (!ReceivedBlockTransactions(block, state, pindex, blockPos, chainparams.GetConsensus())) -
return error("AcceptBlock(): ReceivedBlockTransactions failed"); -
} catch (const std::runtime_error& e) { -
return AbortNode(state, std::string("System error: ") + e.what()); -
} -
FlushStateToDisk(chainparams, state, FlushStateMode::NONE); -
CheckBlockIndex(chainparams.GetConsensus()); -
return true; -
}
(1)校验区块头,找到新区块的区块索引CBlockIndex,如果没有为新区块创建一个索引并加入到mapBlocksIndex映射表;
-
bool CChainState::AcceptBlockHeader(const CBlockHeader& block, CValidationState& state, const CChainParams& chainparams, CBlockIndex** ppindex) { -
AssertLockHeld(cs_main); -
// Check for duplicate -
//新区块的区块hash值 -
uint256 hash = block.GetHash(); -
//检索mapBlockIndex索引表,看区块的索引是否已经在表中 -
BlockMap::iterator miSelf = mapBlockIndex.find(hash); -
CBlockIndex *pindex = nullptr; -
if (hash != chainparams.GetConsensus().hashGenesisBlock) { //非创世区块 -
if (miSelf != mapBlockIndex.end()) { -
// Block header is already known. -
//索引表中已经有区块的索引信息了,返回 -
pindex = miSelf->second; -
if (ppindex) -
*ppindex = pindex; -
if (pindex->nStatus & BLOCK_FAILED_MASK) -
return state.Invalid( -
error("%s: block %s is marked invalid", __func__, hash.ToString()), 0, -
"duplicate"); -
return true; -
} -
//检查区块的工作量是否OK -
if (!CheckBlockHeader(block, state, chainparams.GetConsensus())) -
return error("%s: Consensus::CheckBlockHeader: %s, %s", __func__, hash.ToString(), -
FormatStateMessage(state)); -
// Get prev block index -
//从索引表中查找父区块的索引信息,如果没有父区块的索引,则出错返回 -
CBlockIndex *pindexPrev = nullptr; -
BlockMap::iterator mi = mapBlockIndex.find(block.hashPrevBlock); -
if (mi == mapBlockIndex.end()) -
return state.DoS(10, error("%s: prev block not found", __func__), 0, -
"prev-blk-not-found"); -
pindexPrev = (*mi).second; -
//父区块有问题,出错返回 -
if (pindexPrev->nStatus & BLOCK_FAILED_MASK) -
return state.DoS(100, error("%s: prev block invalid", __func__), REJECT_INVALID, -
"bad-prevblk"); -
if (!ContextualCheckBlockHeader(block, state, chainparams, pindexPrev, GetAdjustedTime())) -
return error("%s: Consensus::ContextualCheckBlockHeader: %s, %s", __func__, -
hash.ToString(), FormatStateMessage(state)); -
// If the previous block index isn't valid, determine if it descends from any block which -
// has been found invalid (g_failed_blocks), then mark pindexPrev and any blocks -
// between them as failed. -
if (!pindexPrev->IsValid(BLOCK_VALID_SCRIPTS)) { -
for (const CBlockIndex *failedit : m_failed_blocks) { -
if (pindexPrev->GetAncestor(failedit->nHeight) == failedit) { -
assert(failedit->nStatus & BLOCK_FAILED_VALID); -
CBlockIndex *invalid_walk = pindexPrev; -
while (invalid_walk != failedit) { -
invalid_walk->nStatus |= BLOCK_FAILED_CHILD; -
setDirtyBlockIndex.insert(invalid_walk); -
invalid_walk = invalid_walk->pprev; -
} -
return state.DoS(100, error("%s: prev block invalid", __func__), REJECT_INVALID, -
"bad-prevblk"); -
} -
} -
} -
} -
//为新区块创建索引,并添加到索引表中 -
if (pindex == nullptr) -
pindex = AddToBlockIndex(block); -
if (ppindex) -
*ppindex = pindex; -
CheckBlockIndex(chainparams.GetConsensus()); -
return true; -
}
主要就是做一些检查,然后为新区块创建索引并添加到索引表中:
-
CBlockIndex* CChainState::AddToBlockIndex(const CBlockHeader& block) -
{ -
AssertLockHeld(cs_main); -
// Check for duplicate -
uint256 hash = block.GetHash(); -
BlockMap::iterator it = mapBlockIndex.find(hash); -
//索引表中已经有区块索引了,直接返回 -
if (it != mapBlockIndex.end()) -
return it->second; -
// Construct new block index object -
//为区块创建索引,并填充相关数据 -
CBlockIndex* pindexNew = new CBlockIndex(block); -
// We assign the sequence id to blocks only when the full data is available, -
// to avoid miners withholding blocks but broadcasting headers, to get a -
// competitive advantage. -
//区块的序号,初始为0,等收到区块完整的数据后在确定该区块真正的***,防止恶意矿工通过只广播区块头,而不发送完整区块获取特权 -
pindexNew->nSequenceId = 0; -
//将索引添加到索引表中 -
BlockMap::iterator mi = mapBlockIndex.insert(std::make_pair(hash, pindexNew)).first; -
//填充区块hash值 -
pindexNew->phashBlock = &((*mi).first); -
BlockMap::iterator miPrev = mapBlockIndex.find(block.hashPrevBlock); -
if (miPrev != mapBlockIndex.end()) -
{ -
//父区块的指针 -
pindexNew->pprev = (*miPrev).second; -
//新区块的高度 -
pindexNew->nHeight = pindexNew->pprev->nHeight + 1; -
pindexNew->BuildSkip(); -
} -
pindexNew->nTimeMax = (pindexNew->pprev ? std::max(pindexNew->pprev->nTimeMax, pindexNew->nTime) : pindexNew->nTime); -
//填充新区块的累积工作量 -
pindexNew->nChainWork = (pindexNew->pprev ? pindexNew->pprev->nChainWork : 0) + GetBlockProof(*pindexNew); -
//将区块的状态升级到BLOCK_VALID_TREE -
pindexNew->RaiseValidity(BLOCK_VALID_TREE); -
//如果新区块的累积工作量比节点收到的最大累积工作量大,更新一下pindexBestHeader全局变量 -
if (pindexBestHeader == nullptr || pindexBestHeader->nChainWork < pindexNew->nChainWork) -
pindexBestHeader = pindexNew; -
setDirtyBlockIndex.insert(pindexNew); -
return pindexNew; -
}
OK,现在新的区块的区块头已经校验完成,新区块的索引也创建好并且加入到了区块索引表mapBlocksIndex中。
(2) 将新区块写入磁盘
区块头校验无误后,新挖出来的区块数据需要保存到磁盘中,之前的代码已经看到过这是怎么做的了:
-
try { -
//将区块存储到磁盘上 -
CDiskBlockPos blockPos = SaveBlockToDisk(block, pindex->nHeight, chainparams, dbp); -
if (blockPos.IsNull()) { -
state.Error(strprintf("%s: Failed to find position to write new block to disk", __func__)); -
return false; -
} -
//标记区块的数据已经接收并且检查过了(区块的状态更新为BLOCK_VALID_TRANSACTIONS) -
if (!ReceivedBlockTransactions(block, state, pindex, blockPos, chainparams.GetConsensus())) -
return error("AcceptBlock(): ReceivedBlockTransactions failed"); -
} catch (const std::runtime_error& e) { -
return AbortNode(state, std::string("System error: ") + e.what()); -
}
首先调用SaveBlockToDisk,将区块数据存盘,然后调用ReceivedBlockTransactions标记区块的数据已校验,将区块的状态升级到BLOCK_VALID_TRANSACTIONS:
-
/** Mark a block as having its data received and checked (up to BLOCK_VALID_TRANSACTIONS). */ -
bool CChainState::ReceivedBlockTransactions(const CBlock &block, CValidationState& state, CBlockIndex *pindexNew, const CDiskBlockPos& pos, const Consensus::Params& consensusParams) -
{ -
//填充区块信息(交易数量,磁盘文件序号等) -
pindexNew->nTx = block.vtx.size(); -
pindexNew->nChainTx = 0; -
pindexNew->nFile = pos.nFile; -
pindexNew->nDataPos = pos.nPos; -
pindexNew->nUndoPos = 0; -
//标记我们已经收到并校验了区块数据 -
pindexNew->nStatus |= BLOCK_HAVE_DATA; -
if (IsWitnessEnabled(pindexNew->pprev, consensusParams)) { -
pindexNew->nStatus |= BLOCK_OPT_WITNESS; -
} -
//区块状态升级为BLOCK_VALID_TRANSACTIONS -
pindexNew->RaiseValidity(BLOCK_VALID_TRANSACTIONS); -
setDirtyBlockIndex.insert(pindexNew); -
if (pindexNew->pprev == nullptr || pindexNew->pprev->nChainTx) { -
// If pindexNew is the genesis block or all parents are BLOCK_VALID_TRANSACTIONS. -
//如果是创世区块或者区块的父区块的状态为BLOCK_VALID_TRANSACTIONS,进入此分支(对于本地挖到的新区块,其父区块一定满足此条件,所以会进入该分支) -
std::deque<CBlockIndex*> queue; -
queue.push_back(pindexNew); -
// Recursively process any descendant blocks that now may be eligible to be connected. -
//这里递归处理mapBlockUnlinked。假设当前mapBlockUnlinked中存在B->C,B->D -
//此时收到了B区块的完整区块,pindexNew指向B区块。 -
//这里会从mapBlockUnlinked中递归删除B->C、B->D,并且将B,C,D加入候选集合setBlockIndexCandidates中 -
while (!queue.empty()) { -
CBlockIndex *pindex = queue.front(); -
queue.pop_front(); -
pindex->nChainTx = (pindex->pprev ? pindex->pprev->nChainTx : 0) + pindex->nTx; -
{ -
LOCK(cs_nBlockSequenceId); -
pindex->nSequenceId = nBlockSequenceId++; -
} -
//对比和区块链顶点区块的工作量,比顶点区块工作量大的区块,加入到候选区块集合中 -
if (chainActive.Tip() == nullptr || !setBlockIndexCandidates.value_comp()(pindex, chainActive.Tip())) { -
setBlockIndexCandidates.insert(pindex); -
} -
//从mapBlocksUnlinked中递归删除 -
std::pair<std::multimap<CBlockIndex*, CBlockIndex*>::iterator, std::multimap<CBlockIndex*, CBlockIndex*>::iterator> range = mapBlocksUnlinked.equal_range(pindex); -
while (range.first != range.second) { -
std::multimap<CBlockIndex*, CBlockIndex*>::iterator it = range.first; -
queue.push_back(it->second); -
range.first++; -
mapBlocksUnlinked.erase(it); -
} -
} -
} else { -
//如果节点的父区块的只有区块头,还未收到完整区块,则加入mapBlocksUnlinked -
if (pindexNew->pprev && pindexNew->pprev->IsValid(BLOCK_VALID_TREE)) { -
mapBlocksUnlinked.insert(std::make_pair(pindexNew->pprev, pindexNew)); -
} -
} -
return true; -
}
上面代码有几点需要注意:
(i) 如果收到了区块C的完整区块,但是它的父区块B只有区块头,则会在mapBlocksUnlinked表中添加一项B->C,表示新区块C已经有完整数据,但其父区块尚不完整;
(ii) 假设当前mapBlocksUnlinked表中包含B->C, B->D,也就是说收到了B,C,D的区块头,其中区块C和区块D都以区块B为父区块(区块链分叉,比如两个矿工在几乎同一时间挖出了C和D),然后节点收到了C和D的完整区块,此时mapBlocksUnlinked中就会存在B->C,B->D两项。一段时间后收到了B区块的完整区块,此时将B->C和B->D移除,并且将B、C、D加入到setBlockIndexCandiates中作为候选区块;
2.4.3 延长区块链
经过之前的步骤,新挖出来的区块已经加入到候选区块集合中,接下来就要尝试去将新区块添加到区块链上了,ProcessNewBlock的最后:
-
//将新区块加入到本地区块链,延长本地最长(具有最大工作量)的链 -
if (!g_chainstate.ActivateBestChain(state, chainparams, pblock)) -
return error("%s: ActivateBestChain failed (%s)", __func__, FormatStateMessage(state));
通过ActivateBestChain来将新区块加入到区块链上:
-
/** -
* Make the best chain active, in multiple steps. The result is either failure -
* or an activated best chain. pblock is either nullptr or a pointer to a block -
* that is already loaded (to avoid loading it again from disk). -
* -
* ActivateBestChain is split into steps (see ActivateBestChainStep) so that -
* we avoid holding cs_main for an extended period of time; the length of this -
* call may be quite long during reindexing or a substantial reorg. -
*/ -
bool CChainState::ActivateBestChain(CValidationState &state, const CChainParams& chainparams, std::shared_ptr<const CBlock> pblock) { -
// Note that while we're often called here from ProcessNewBlock, this is -
// far from a guarantee. Things in the P2P/RPC will often end up calling -
// us in the middle of ProcessNewBlock - do not assume pblock is set -
// sanely for performance or correctness! -
AssertLockNotHeld(cs_main); -
CBlockIndex *pindexMostWork = nullptr; -
CBlockIndex *pindexNewTip = nullptr; -
int nStopAtHeight = gArgs.GetArg("-stopatheight", DEFAULT_STOPATHEIGHT); -
do { -
boost::this_thread::interruption_point(); -
if (GetMainSignals().CallbacksPending() > 10) { -
// Block until the validation queue drains. This should largely -
// never happen in normal operation, however may happen during -
// reindex, causing memory blowup if we run too far ahead. -
SyncWithValidationInterfaceQueue(); -
} -
const CBlockIndex *pindexFork; -
bool fInitialDownload; -
{ -
LOCK(cs_main); -
ConnectTrace connectTrace(mempool); // Destructed before cs_main is unlocked -
//拿到当前区块链的顶点 -
CBlockIndex *pindexOldTip = chainActive.Tip(); -
if (pindexMostWork == nullptr) { -
//查找工作量最大的分支的顶点区块 -
pindexMostWork = FindMostWorkChain(); -
} -
// Whether we have anything to do at all. -
//如果区块链当前的顶点已经是最大工作量,不用做任何事情,返回 -
if (pindexMostWork == nullptr || pindexMostWork == chainActive.Tip()) -
return true; -
bool fInvalidFound = false; -
std::shared_ptr<const CBlock> nullBlockPtr; -
//将找到的工作量最大的区块加入到区块链中 -
if (!ActivateBestChainStep(state, chainparams, pindexMostWork, pblock && pblock->GetHash() == pindexMostWork->GetBlockHash() ? pblock : nullBlockPtr, fInvalidFound, connectTrace)) -
return false; -
if (fInvalidFound) { -
// Wipe cache, we may need another branch now. -
pindexMostWork = nullptr; -
} -
pindexNewTip = chainActive.Tip(); -
pindexFork = chainActive.FindFork(pindexOldTip); -
fInitialDownload = IsInitialBlockDownload(); -
for (const PerBlockConnectTrace& trace : connectTrace.GetBlocksConnected()) { -
assert(trace.pblock && trace.pindex); -
GetMainSignals().BlockConnected(trace.pblock, trace.pindex, trace.conflictedTxs); -
} -
//通知区块链更新 -
// Notify external listeners about the new tip. -
// Enqueue while holding cs_main to ensure that UpdatedBlockTip is called in the order in which blocks are connected -
GetMainSignals().UpdatedBlockTip(pindexNewTip, pindexFork, fInitialDownload); -
// Always notify the UI if a new block tip was connected -
if (pindexFork != pindexNewTip) { -
uiInterface.NotifyBlockTip(fInitialDownload, pindexNewTip); -
} -
} -
// When we reach this point, we switched to a new tip (stored in pindexNewTip). -
if (nStopAtHeight && pindexNewTip && pindexNewTip->nHeight >= nStopAtHeight) StartShutdown(); -
// We check shutdown only after giving ActivateBestChainStep a chance to run once so that we -
// never shutdown before connecting the genesis block during LoadChainTip(). Previously this -
// caused an assert() failure during shutdown in such cases as the UTXO DB flushing checks -
// that the best block hash is non-null. -
if (ShutdownRequested()) -
break; -
} while (pindexNewTip != pindexMostWork); -
CheckBlockIndex(chainparams.GetConsensus()); -
// Write changes periodically to disk, after relay. -
if (!FlushStateToDisk(chainparams, state, FlushStateMode::PERIODIC)) { -
return false; -
} -
return true; -
}
2.4.3.1 查找具有最大工作量的分支
通过FindMostWorkChain查找具有最大工作量的区块,考虑之前的例子:
假设当前的区块链具有如下形态:
A —— B
\
C ------- D
B是当前节点区块链的顶点,B和C区块都以A为父区块(可能有两个矿工几乎同时挖到了区块B和C,节点先收到了区块B,因此以区块B来延长了区块链),此时节点收到了区块D,D的父区块为C,因为D的工作量要比B高,因此节点需要切换最长链到A、C、D,具体做法是:
先从区块链上断开区块B,然后区块链的顶点回退到区块A,最后依次在连接区块C和D到区块链上;
来看看如何查找最大工作量的区块的:
-
/** -
* Return the tip of the chain with the most work in it, that isn't -
* known to be invalid (it's however far from certain to be valid). -
*/ -
CBlockIndex* CChainState::FindMostWorkChain() { -
do { -
CBlockIndex *pindexNew = nullptr; -
// Find the best candidate header. -
//从候选区块集合中找到工作量最大的候选区块 -
{ -
std::set<CBlockIndex*, CBlockIndexWorkComparator>::reverse_iterator it = setBlockIndexCandidates.rbegin(); -
if (it == setBlockIndexCandidates.rend()) -
return nullptr; -
pindexNew = *it; -
} -
// Check whether all blocks on the path between the currently active chain and the candidate are valid. -
// Just going until the active chain is an optimization, as we know all blocks in it are valid already. -
CBlockIndex *pindexTest = pindexNew; -
bool fInvalidAncestor = false; -
//从选出的具有最大工作量的候选区块开始回朔,一直回朔到存在于区块链上的祖先区块为止。 -
//例如当前区块链顶点是A,然后候选区块按工作量依次是B,C,D,B是C的父区块,C是D的父区块, -
//则从D开始回朔,一直到区块A为止。setBlockIndexCandidates中包含<B, C, D> -
while (pindexTest && !chainActive.Contains(pindexTest)) { -
assert(pindexTest->nChainTx || pindexTest->nHeight == 0); -
// Pruned nodes may have entries in setBlockIndexCandidates for -
// which block files have been deleted. Remove those as candidates -
// for the most work chain if we come across them; we can't switch -
// to a chain unless we have all the non-active-chain parent blocks. -
//区块是否是一个坏区块 -
bool fFailedChain = pindexTest->nStatus & BLOCK_FAILED_MASK; -
//是否尚未收到完整区块 -
bool fMissingData = !(pindexTest->nStatus & BLOCK_HAVE_DATA); -
if (fFailedChain || fMissingData) { -
// Candidate chain is not usable (either invalid or missing data) -
if (fFailedChain && (pindexBestInvalid == nullptr || pindexNew->nChainWork > pindexBestInvalid->nChainWork)) -
pindexBestInvalid = pindexNew; -
//假设从A->D的路径上,当回朔到区块B时发现B的完整区块尚未收到,则向mapBlocksUnlinked中添加<B,C>,<C,D>, -
//同时将B、C和D从候选区块集合setBlockIndexCandidates中移除 -
CBlockIndex *pindexFailed = pindexNew; -
// Remove the entire chain from the set. -
while (pindexTest != pindexFailed) { -
if (fFailedChain) { -
pindexFailed->nStatus |= BLOCK_FAILED_CHILD; -
} else if (fMissingData) { -
// If we're missing data, then add back to mapBlocksUnlinked, -
// so that if the block arrives in the future we can try adding -
// to setBlockIndexCandidates again. -
mapBlocksUnlinked.insert(std::make_pair(pindexFailed->pprev, pindexFailed)); -
} -
setBlockIndexCandidates.erase(pindexFailed); -
pindexFailed = pindexFailed->pprev; -
} -
setBlockIndexCandidates.erase(pindexTest); -
fInvalidAncestor = true; -
//跳出本轮循环 -
break; -
} -
//向前回朔到父区块 -
pindexTest = pindexTest->pprev; -
} -
if (!fInvalidAncestor) -
return pindexNew; -
} while(true);
注意这个算法:首先是从候选区块集合中工作量最大的候选区块开始,选定一个区块B[n],然后沿着父区块向前回朔,一旦路径上出现未收到完整数据的区块(假设为B[m], m < n),则将B[m]到B[n]全部从候选区块集合中移除,同时在mapBlockUnlinked中添加B[m]->B[m+1],....,B[n-1]->B[n],这样的目的是当将来收到完整区块B[m]以后,可以重新将B[m]....B[n]加入到候选区块中。
用文字说明可能不是很清楚,这里再举个例子,如下图:
图中A为当前区块链的顶点,此时的区块链存在三条分叉:G3->A,F2->A,E->A,其中工作量G3>F2>E
寻找具有最大工作量的分叉时,先从候选集合中的G3开始:
(1) 从G3沿着父区块指针回朔,当回朔到G1的时候,发现G1的数据还没有到(nStatus & BLOCK_HAVE_DATA不成立),于是将G3、G2、G1从setBlockIndexCandidates中移除,并且在mapBlocksUnlinked中添加<G2,G3>,<G1,G2>;
(2) 然后选定工作量次小的候选区块F2进行同样的操作,这一次由于从F2->A的路径上所有的区块都正常并且都有数据,于是FindMostWorkChain返回区块F2,从而F2->F1->A就是本次找到的最大工作量的分叉;
(3) 切换主链,具有最大工作量的F2->F1->A成为新的主链,F2为新的区块链的顶点,将候选区块集合中的F1,F2以及工作量跟小的E移除;
(4) 一段时间后,区块G1的数据到达了,于是递归移除mapBlocksUnlinked中的<G1,G2>,<G2,G3>项,同时将G1,G2,G3添加到候选区块集合中;
(5) 这一次由于G3的工作量最大,切换到G3->A为新的主链,将原来主链上的F2,F1从区块链上断开(DisconnectBlock)
2.4.3.2 **最长链
当FindMostWorkChain找到最大工作量的区块链分支后,就需要将主链切换到该分支上。存在几种情况:
(1) 区块链当前已经具有最大工作量(区块链的顶点已经是最大工作量的区块了),什么都不需要做:
-
//如果区块链当前的顶点已经是最大工作量,不用做任何事情,返回 -
if (pindexMostWork == nullptr || pindexMostWork == chainActive.Tip()) -
return true;
(2) 其他的情况下,调用ActivateBestChainStep来处理:
-
//将区块加入到区块链中 -
if (!ActivateBestChainStep(state, chainparams, pindexMostWork, pblock && pblock->GetHash() == pindexMostWork->GetBlockHash() ? pblock : nullBlockPtr, fInvalidFound, connectTrace)) -
return false;
来看看ActivateBestChainStep的处理过程:
-
/** -
* Try to make some progress towards making pindexMostWork the active block. -
* pblock is either nullptr or a pointer to a CBlock corresponding to pindexMostWork. -
*/ -
bool CChainState::ActivateBestChainStep(CValidationState& state, const CChainParams& chainparams, CBlockIndex* pindexMostWork, const std::shared_ptr<const CBlock>& pblock, bool& fInvalidFound, ConnectTrace& connectTrace) -
{ -
AssertLockHeld(cs_main); -
//当前区块链的顶点 -
const CBlockIndex *pindexOldTip = chainActive.Tip(); -
//如果区块链有分叉,找到分叉点 -
const CBlockIndex *pindexFork = chainActive.FindFork(pindexMostWork); -
// Disconnect active blocks which are no longer in the best chain. -
//从分叉点到当前区块链顶点之间的区块从主链上全部断开 -
bool fBlocksDisconnected = false; -
DisconnectedBlockTransactions disconnectpool; -
while (chainActive.Tip() && chainActive.Tip() != pindexFork) { -
if (!DisconnectTip(state, chainparams, &disconnectpool)) { -
// This is likely a fatal error, but keep the mempool consistent, -
// just in case. Only remove from the mempool in this case. -
//断开区块过程中发生错误,需要更新交易池 -
UpdateMempoolForReorg(disconnectpool, false); -
return false; -
} -
fBlocksDisconnected = true; -
} -
// Build list of new blocks to connect. -
std::vector<CBlockIndex*> vpindexToConnect; -
bool fContinue = true; -
int nHeight = pindexFork ? pindexFork->nHeight : -1; -
while (fContinue && nHeight != pindexMostWork->nHeight) { -
// Don't iterate the entire list of potential improvements toward the best tip, as we likely only need -
// a few blocks along the way. -
//计算出需要添加到区块链上的区块 -
int nTargetHeight = std::min(nHeight + 32, pindexMostWork->nHeight); -
vpindexToConnect.clear(); -
vpindexToConnect.reserve(nTargetHeight - nHeight); -
CBlockIndex *pindexIter = pindexMostWork->GetAncestor(nTargetHeight); -
while (pindexIter && pindexIter->nHeight != nHeight) { -
vpindexToConnect.push_back(pindexIter); -
pindexIter = pindexIter->pprev; -
} -
nHeight = nTargetHeight; -
// Connect new blocks. -
//将新区块连接到区块链上 -
for (CBlockIndex *pindexConnect : reverse_iterate(vpindexToConnect)) { -
if (!ConnectTip(state, chainparams, pindexConnect, pindexConnect == pindexMostWork ? pblock : std::shared_ptr<const CBlock>(), connectTrace, disconnectpool)) { -
if (state.IsInvalid()) { -
// The block violates a consensus rule. -
if (!state.CorruptionPossible()) -
InvalidChainFound(vpindexToConnect.back()); -
state = CValidationState(); -
fInvalidFound = true; -
fContinue = false; -
break; -
} else { -
// A system error occurred (disk space, database error, ...). -
// Make the mempool consistent with the current tip, just in case -
// any observers try to use it before shutdown. -
//更新内存交易池 -
UpdateMempoolForReorg(disconnectpool, false); -
return false; -
} -
} else { -
//新区块成功添加到了区块链上,将候选区块集合中所有比新区块工作量少的区块移除 -
PruneBlockIndexCandidates(); -
if (!pindexOldTip || chainActive.Tip()->nChainWork > pindexOldTip->nChainWork) { -
// We're in a better position than we were. Return temporarily to release the lock. -
fContinue = false; -
break; -
} -
} -
} -
} -
//如果有断开的区块,需要更新一下内存交易池 -
if (fBlocksDisconnected) { -
// If any blocks were disconnected, disconnectpool may be non empty. Add -
// any disconnected transactions back to the mempool. -
UpdateMempoolForReorg(disconnectpool, true); -
} -
mempool.check(pcoinsTip.get()); -
// Callbacks/notifications for a new best chain. -
if (fInvalidFound) -
CheckForkWarningConditionsOnNewFork(vpindexToConnect.back()); -
else -
CheckForkWarningConditions(); -
return true; -
}
这段代码的逻辑可以分几种情况来说明:
(i) 区块链没有分叉,新区块直接延长当前区块链:
A --- B
假设当前区块链顶点为A,B是找到的具有最大工作量的新区块,直接将B调用ConnectTip将新区块连到区块链上,B成为新的顶点;
(ii) 区块链存在分叉:
假设区块链的状态如下:
A —— B
\
C -------- D
当前区块链的顶点是B,然后经过查找找到了最大工作量的区块为D。
此时通过FindFork函数,找到了区块链的分叉点区块A,于是将区块B从区块链断开(DisconnectBlock),同时依次将区块C和D添加到区块链上(ConnectTip),D成为了新的区块链顶点,于是新的分支A-C-D成为了最长链被**。
查找分叉点的方法为FindFork:
-
const CBlockIndex *CChain::FindFork(const CBlockIndex *pindex) const { -
if (pindex == nullptr) { -
return nullptr; -
} -
//找到分支上和当前主链的顶点区块高度相同的区块P -
if (pindex->nHeight > Height()) -
pindex = pindex->GetAncestor(Height()); -
//从上一步找到的区块P开始向前回朔,直到找到一个存在于当前主链上的区块为止 -
while (pindex && !Contains(pindex)) -
pindex = pindex->pprev; -
return pindex; -
}
算法很简单,假设:
A — B — F
\
C -- D ------ E
A-B-F是当前主链,A的高度为100,B为101,F为102,分叉C,D,E高度,依次是101,102,103,则首先定位到E的祖先D(因为D和主链的顶点区块F具有相同高度),然后从D开始沿着父区块指针回朔一直到A,于是找到了区块链的分叉点区块A。
2.4.4.3 节点间的交互
目前为止我们分析的都是当前节点挖到区块然后将区块加入本地区块链的过程。节点挖出区块以后,还需要将新区块广播到网络中扩散出去,让全网对新区块达成共识。
(1) 广播区块头
之前分析AcceptBlock方法的时候,有一段代码我们没做说明:
-
// Header is valid/has work, merkle tree and segwit merkle tree are good...RELAY NOW -
// (but if it does not build on our best tip, let the SendMessages loop relay it) -
if (!IsInitialBlockDownload() && chainActive.Tip() == pindex->pprev) -
GetMainSignals().NewPoWValidBlock(pindex, pblock);
当调用AcceptBlockHeader校验完区块头,没有错误(工作量OK,merkel树的hash正确...),并且新区块的父区块就是当前区块链的顶点区块(对于新挖到的区块,一般都是成立的),则通过NewPowValidBlock将区块头广播出去,最终调用PeerLogicValidation::NewPoWValidBlock:
-
void PeerLogicValidation::NewPoWValidBlock(const CBlockIndex *pindex, const std::shared_ptr<const CBlock>& pblock) { -
//将区块头、区块交易id打包到CBlockHeaderAndShortTxIDs中 -
std::shared_ptr<const CBlockHeaderAndShortTxIDs> pcmpctblock = std::make_shared<const CBlockHeaderAndShortTxIDs> (*pblock, true); -
const CNetMsgMaker msgMaker(PROTOCOL_VERSION); -
LOCK(cs_main); -
//更新相关数据 -
static int nHighestFastAnnounce = 0; -
if (pindex->nHeight <= nHighestFastAnnounce) -
return; -
nHighestFastAnnounce = pindex->nHeight; -
bool fWitnessEnabled = IsWitnessEnabled(pindex->pprev, Params().GetConsensus()); -
uint256 hashBlock(pblock->GetHash()); -
{ -
LOCK(cs_most_recent_block); -
most_recent_block_hash = hashBlock; -
most_recent_block = pblock; -
most_recent_compact_block = pcmpctblock; -
fWitnessesPresentInMostRecentCompactBlock = fWitnessEnabled; -
} -
//对所有相邻的节点发送CMPCTBLOCK消息,将区块头广播出去 -
connman->ForEachNode([this, &pcmpctblock, pindex, &msgMaker, fWitnessEnabled, &hashBlock](CNode* pnode) { -
// TODO: Avoid the repeated-serialization here -
if (pnode->nVersion < INVALID_CB_NO_BAN_VERSION || pnode->fDisconnect) -
return; -
ProcessBlockAvailability(pnode->GetId()); -
CNodeState &state = *State(pnode->GetId()); -
// If the peer has, or we announced to them the previous block already, -
// but we don't think they have this one, go ahead and announce it -
if (state.fPreferHeaderAndIDs && (!fWitnessEnabled || state.fWantsCmpctWitness) && -
!PeerHasHeader(&state, pindex) && PeerHasHeader(&state, pindex->pprev)) { -
LogPrint(BCLog::NET, "%s sending header-and-ids %s to peer=%d\n", "PeerLogicValidation::NewPoWValidBlock", -
hashBlock.ToString(), pnode->GetId()); -
connman->PushMessage(pnode, msgMaker.Make(NetMsgType::CMPCTBLOCK, *pcmpctblock)); -
state.pindexBestHeaderSent = pindex; -
} -
}); -
}
可以看到,最后发送CMPCTBLOCK消息,将区块头广播出去。
注意这里之所以不广播完整区块,而只广播区块头和交易id,是为了节省带宽,因为一个完整的区块的数据量是很可观的,如果直接广播整个区块会占用很大的带宽,拉低整体处理速度。
(2) 节点接收广播
其他节点会收到CMPCTBLOCK广播消息,来看看节点收到其他节点广播的区块头以后是如何处理的,位于net_processing.cpp的ProcessMessage中,这里的处理逻辑比较复杂,本文只大概描述一下关键点,读者可以自行阅读CMPCTBLOCK消息的分支对应的源码。
节点收到CMPCTBLOCK消息,同样会调用到AcceptBlockHeader校验区块头,如果区块正确(工作量、merkel树hash等),就创建索引并加入到区块索引表中;
之后节点会为新区块生成一个QueuedBlock并添加到队列中,等待区块交易数据的到来,当区块的交易数据到来时,节点将受到BLOCKTXN消息,这条消息的处理如下:
-
else if (strCommand == NetMsgType::BLOCKTXN && !fImporting && !fReindex) // Ignore blocks received while importing -
{ -
BlockTransactions resp; -
vRecv >> resp; -
std::shared_ptr<CBlock> pblock = std::make_shared<CBlock>(); -
bool fBlockRead = false; -
{ -
LOCK(cs_main); -
//从队列中找到消息中指定的区块对应的QueuedBlock,没有找到则返回 -
std::map<uint256, std::pair<NodeId, std::list<QueuedBlock>::iterator> >::iterator it = mapBlocksInFlight.find(resp.blockhash); -
if (it == mapBlocksInFlight.end() || !it->second.second->partialBlock || -
it->second.first != pfrom->GetId()) { -
LogPrint(BCLog::NET, "Peer %d sent us block transactions for block we weren't expecting\n", pfrom->GetId()); -
return true; -
} -
//填充交易信息 -
PartiallyDownloadedBlock& partialBlock = *it->second.second->partialBlock; -
ReadStatus status = partialBlock.FillBlock(*pblock, resp.txn); -
if (status == READ_STATUS_INVALID) { -
MarkBlockAsReceived(resp.blockhash); // Reset in-flight state in case of whitelist -
Misbehaving(pfrom->GetId(), 100, strprintf("Peer %d sent us invalid compact block/non-matching block transactions\n", pfrom->GetId())); -
return true; -
} else if (status == READ_STATUS_FAILED) { -
// Might have collided, fall back to getdata now :( -
std::vector<CInv> invs; -
invs.push_back(CInv(MSG_BLOCK | GetFetchFlags(pfrom), resp.blockhash)); -
connman->PushMessage(pfrom, msgMaker.Make(NetMsgType::GETDATA, invs)); -
} else { -
// Block is either okay, or possibly we received -
// READ_STATUS_CHECKBLOCK_FAILED. -
// Note that CheckBlock can only fail for one of a few reasons: -
// 1. bad-proof-of-work (impossible here, because we've already -
// accepted the header) -
// 2. merkleroot doesn't match the transactions given (already -
// caught in FillBlock with READ_STATUS_FAILED, so -
// impossible here) -
// 3. the block is otherwise invalid (eg invalid coinbase, -
// block is too big, too many legacy sigops, etc). -
// So if CheckBlock failed, #3 is the only possibility. -
// Under BIP 152, we don't DoS-ban unless proof of work is -
// invalid (we don't require all the stateless checks to have -
// been run). This is handled below, so just treat this as -
// though the block was successfully read, and rely on the -
// handling in ProcessNewBlock to ensure the block index is -
// updated, reject messages go out, etc. -
//一切正常,标记区块的交易已经收到,将区块对应的QueuedBlock从队列中移除掉 -
MarkBlockAsReceived(resp.blockhash); // it is now an empty pointer -
fBlockRead = true; -
// mapBlockSource is only used for sending reject messages and DoS scores, -
// so the race between here and cs_main in ProcessNewBlock is fine. -
// BIP 152 permits peers to relay compact blocks after validating -
// the header only; we should not punish peers if the block turns -
// out to be invalid. -
mapBlockSource.emplace(resp.blockhash, std::make_pair(pfrom->GetId(), false)); -
} -
} // Don't hold cs_main when we call into ProcessNewBlock -
//如果正确的将收到的交易填充到区块中,调用ProcessNewBlock处理之 -
if (fBlockRead) { -
bool fNewBlock = false; -
// Since we requested this block (it was in mapBlocksInFlight), force it to be processed, -
// even if it would not be a candidate for new tip (missing previous block, chain not long enough, etc) -
// This bypasses some anti-DoS logic in AcceptBlock (eg to prevent -
// disk-space attacks), but this should be safe due to the -
// protections in the compact block handler -- see related comment -
// in compact block optimistic reconstruction handling. -
//处理新区块 -
ProcessNewBlock(chainparams, pblock, /*fForceProcessing=*/true, &fNewBlock); -
if (fNewBlock) { -
pfrom->nLastBlockTime = GetTime(); -
} else { -
LOCK(cs_main); -
mapBlockSource.erase(pblock->GetHash()); -
} -
} -
}
可以看到,最终也是调用ProcessNewBlock来处理新区块,前面几节已经花了较大篇幅来分析这个函数。
最后总结一下这里的时序:
2.5 PoW共识过程总结
因为共识是区块链的核心所在,本节通过例子来演示一下上面代码中达成共识的过程。
假设两个节点N1和N2,当前各自区块链的顶点都是区块A:
随后,假设两个矿工都以A为父区块,几乎在同一时间挖出了区块B和C,然后节点P1先收到了区块B,并用区块B延长了自己的区块链,而节点P2节点先收到了区块C,于是P2节点用区块C延长了自己的区块链,于是出现了分叉,两个节点各自维护的区块链开始不一致了:
不久后,节点P1也收到了区块C,而节点P2收到了区块B。于是节点P1执行ProcessNewBlock的逻辑,为区块C创建索引并添加到自己的索引表中,同时将区块C添加到候选区块集合中,同样的,节点P2也为区块B创建索引并加到自己的索引表中,并把区块B添加到候选区块集合:
之后,有矿工在区块C的基础上挖出了新区块D,按照之前代码的分析,P2会按如下情况处理:
为区块D创建索引并加入索引表(AcceptBlockHeader);
将区块加入候选区块集合(AcceptBlock),此时候选区块集合为{B,D}
找到最大工作量节点D,寻找分叉点,因为没有分叉,所以最后直接将区块D连接到区块链上,区块D成为新的区块顶点(ActivateBestChain),同时将候选区块集合中的区块D以及所有比区块D工作量小的区块移除,于是候选集合为空;最终,节点P2的状态如下:
而节点P1收到区块D以后的处理:
首先为区块D创建区块索引并加入到索引表(AcceptBlockHeader);
将区块D加入到候选区块集合,此时候选区块集合{ C, D } (AcceptBlock);
候选区块D拥有最大工作量,寻找分叉点,因为节点P1存在分叉,于是像下图:
旧的区块链顶点区块B将被断开,区块D和C将挂到区块链上(ActivateBestChain),节点P1从原来的主链A-B切换到新的最长链A-C-D,节点P1和P2的区块链重新达成了一致:
3 小结
PoW共识是比特币区块链的核心之一,本文结合源码分析了比特币通过PoW算法就区块链的状态达成最终一致的过程进行了分析,看会了PoW的实现,可以说比特币区块链的一块硬骨头就算啃下来了。
(1) 节点基于当前的区块链构造出候选区块,矿工挖出候选区块的奖励费=区块包含的交易的交易费 + 系统的比特币奖励,其中系统的比特币奖励初始为50个比特币,然后每产出210000个区块(约4年)就减半;
(2) 节点通过不断递增新区块头部的nonce值计算区块头部的hash,直到hash满足新区块的难度目标值为止;
(3) 节点用新区块延长本地区块链,同时将新区块广播出去;
(4) 收到新区块的节点校验区块,并将区块加到自己的区块链上;
(5) 分析了区块链分叉以及当存在分叉时节点如何切换到最长链的;
另外学习的时候,需要理解本文开始提到的几个和区块链相关的数据结构和变量的作用,才能更好的理解比特币区块链PoW的过程;