Skip to content

Latest commit

 

History

History
338 lines (280 loc) · 13.5 KB

精通比特币之七挖矿和共识.md

File metadata and controls

338 lines (280 loc) · 13.5 KB

挖矿和共识


一、挖矿和工作量证明算法


前面基本把区块链的交易加密啥的都过了一次,现在过大家比较关心的挖矿和共识,其实挖矿和共识放在一起做为一章,是因为二者是不好分开的,挖矿的过程过程其实也是一个变相等待共识的过程。哪个先挖矿出来,生成最新的区块,就会得到奖励,包括创币奖励和交易费奖励。只不过创币奖励会越来越少,直到没有。
挖矿并不是真正的去矿山开采,而是用电脑的CPU,GPU或者专门的芯片甚至由专门的芯片组成的机器群俗称矿池来做一种哈希运算,前面提到过,其实就是对区块头进行哈希算法,直到这个算法的结果满足设定的难度需求。
在新的源码中,挖矿分成了两部分,即基本的CPU挖矿和使用独立的挖矿单元来挖矿,主要分成了以下两种方法
1、直接挖矿:
目前基本没人挖矿了,因为基本挖不到,但是代码得有:
struct CBlockTemplate
{
    CBlock block;
    std::vector<CAmount> vTxFees;
    std::vector<int64_t> vTxSigOpsCost;
    std::vector<unsigned char> vchCoinbaseCommitment;
};
//利用上面的模板生成相关的区块,并填充数据-下面的函数为通用的产生新块的函数在miner.cpp 中。
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
    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);
    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.
    CMutableTransaction coinbaseTx;
    coinbaseTx.vin.resize(1);
    coinbaseTx.vin[0].prevout.SetNull();
    coinbaseTx.vout.resize(1);
    coinbaseTx.vout[0].scriptPubKey = scriptPubKeyIn;
    //费用+减半机制得到的创币奖励
    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
    pblock->hashPrevBlock  = pindexPrev->GetBlockHash();
    UpdateTime(pblock, chainparams.GetConsensus(), pindexPrev);//更新难度,这个函数里调用下文的难度计算函数
    pblock->nBits          = GetNextWorkRequired(pindexPrev, pblock, chainparams.GetConsensus());
    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);
}

上面的函数会被两个地方调用,一个是generateBlocks,一个是getblocktemplate,这两个函数都在mining.cpp中。这就到了RPC的模块范围内了。大家应该明白这个应该是供外部调用了。
getblocktemplate这个主要是提供给现在真正的挖矿的矿池和矿机用的,芯片上的软件通过不断的调用它,然后通过芯片硬件的HASH计算来得到结果并填充这个区块来产生真正的区块。
重点说一下前者,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();
        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);
            //处理Nonce
            IncrementExtraNonce(pblock, chainActive.Tip(), nExtraNonce);
        }
        //真正的循环计算哈希并进行验证
        while (nMaxTries > 0 && pblock->nNonce < nInnerLoopCount && !CheckProofOfWork(pblock->GetHash(), pblock->nBits, Params().GetConsensus())) {
            ++pblock->nNonce;
            --nMaxTries;
        }
        if (nMaxTries == 0) {
            break;
        }
        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;
}

pblock->GetHash()负责不断的对头进行哈希取值,然后进行CheckProofOfWork,不对的话nonce++,再来,直到条件终止。真正的挖矿,就在这行代码里的这个函数里。

二、共识


下来就开始共识的部分了:
bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params& params)
{
    bool fNegative;
    bool fOverflow;
    arith_uint256 bnTarget;

    //bBits,是当前块中的难度值
    bnTarget.SetCompact(nBits, &fNegative, &fOverflow);

    // 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;
}

params.powLimit是原始的难度,即下文提到的最小难度,bnTarget这个就是真正的计算难度。如果和新挖区块的难度比较,小则返回False,大则表明挖矿成功。但是这个难度不是一成不变的:
//nFirstBlockTime即前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;
    if (nActualTimespan < params.nPowTargetTimespan/4)//不小于3.5天
        nActualTimespan = params.nPowTargetTimespan/4;
    if (nActualTimespan > params.nPowTargetTimespan*4)//不大于56天
        nActualTimespan = params.nPowTargetTimespan*4;

    // Retarget
    const arith_uint256 bnPowLimit = UintToArith256(params.powLimit);
    arith_uint256 bnNew;
    bnNew.SetCompact(pindexLast->nBits);
    //计算前2016个块的难度总和,即单个块的难度*总时间
    bnNew \*= nActualTimespan;
    //计算新的难度,即2016个块的难度总和/14天的秒数
    bnNew /= params.nPowTargetTimespan;

    if (bnNew > bnPowLimit)
        bnNew = bnPowLimit;

    return bnNew.GetCompact();
}

比特币计算新的难度保存在新块的nBits中,这个上面提到了。说一下计算的步骤

1、找到前2016个块的第一个块。

2、计算生成这2016个块花费的时间,即最后一个块的时间与第一个块的时间差。时间差在3.5~56天之间。

3、计算前2016块的难度总和,即单个块的难度*时间

4、计算新难度,即难度总和/14天的秒数,得到每秒的难度值。

5、新的难度不能大于参数定义的最小难度。
在测试网络和主网略有不同。
原来比特币系统提供了一个函数计算一段时间内的最小难度,ComputeMinWork, 新版本应该是没有了,但还是有一个参数设置了最小难度,即上文提到的params.powLimit,它的作用如下:

1、在测试网络中如果时间区间大于28天,则最小难度为它,即参数定义的最小难度。

2、时间以56天递减,难度4倍递增,循环计算,计算出新难度。

3、新难度不能大于这个最小难度。

三、组装到链


挖矿和共识完成后,就是挂到主链上,保存,更新索引,各种验证,然后上网传播区块了,这个在交易和网络中都有涉及,这里就不再细说。只说明一下流程:
UniValue generateBlocks(std::shared_ptr<CReserveScript> coinbaseScript, int nGenerate, uint64_t nMaxTries, bool keepScript)
{
......
        if (!ProcessNewBlock(Params(), shared_pblock, true, nullptr))
            throw JSONRPCError(RPC_INTERNAL_ERROR, "ProcessNewBlock, block not accepted");
......
}
bool ProcessNewBlock(const CChainParams& chainparams, const std::shared_ptr<const CBlock> pblock, bool fForceProcessing, bool *fNewBlock)
{
......
    {

        if (ret) {
            // Store to disk
            ret = g_chainstate.AcceptBlock(pblock, state, chainparams, &pindex, fForceProcessing, nullptr, fNewBlock);
        }
        if (!ret) {
            //如果失败就处理一下异常
            GetMainSignals().BlockChecked(\*pblock, state);
        }
    }
}
......

bool CChainState::AcceptBlock(const std::shared_ptr<const CBlock>& pblock, CValidationState& state, const CChainParams& chainparams, CBlockIndex** ppindex, bool fRequested, const CDiskBlockPos* dbp, bool* fNewBlock)
{
......

    // 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);
......
}

/**
 * Maintain state about the best-seen block and fast-announce a compact block
 * to compatible peers.
 */
 //从BIP152开始使用紧凑块,提升传输速度,减少传送亘。
void PeerLogicValidation::NewPoWValidBlock(const CBlockIndex *pindex, const std::shared_ptr<const CBlock>& pblock) {
  ......
            connman->PushMessage(pnode, msgMaker.Make(NetMsgType::CMPCTBLOCK, *pcmpctblock));
            state.pindexBestHeaderSent = pindex;
......
}

从上面可以看到,在创建出一个新块后,直接调用ProcessNewBlock来处理它(这里把挂到主链上的代码忽略掉了,主要是为了突出网络发送的流程)。在AcceptBlock函数中,调用GetMainSignals().NewPoWValidBlock发送紧凑块,提升性能。而在发送中看到前面熟悉的PushMessage,这个就直接从网络广播出去了。

四、分叉


分叉有软硬之分,软分叉一般是软件升级造成的版本不同出现的,一般来,大家把版本升级后就会自动融合。硬分叉一般是共识机制出现了重大变化,造成区块分叉。