大一就知道区块链了,还挖了几天莱特币,虽然没有赚到钱,不过当时还是觉得很神奇的(现在也是),一直没有时间学习相关理论,这次既然是作业就好好写下吧。
本篇文章首先介绍比特币中的基础知识,和挖矿相关的知识,然后讨论一些挖矿相关的常见问题。

一、区块链中的存储方式

要想了解比特币的内部原理,就非常有必要知道区块链是如何存储数据的。我们知道,区块链是一个去中心分布式记账系统,其中非常重要的一点就是如何达成分布式共识。比特币中采用哈希指针链接的方式链接每一个区块,这样一来,任何加入到链中的区块就可以很容易的验证数据是否被篡改过,同时,区块中的数据,也就是交易信息,采用Merkle tree的方式组织,区块头只需要维护这个Merkle tree根哈希值即可。

1) 哈希指针

哈希指针是一种数据结构,这种数据结构在我们即将讨论的很多系统中都很有用。简单来说,哈希指针是一个指向数据存储位置及其位置数据的哈希值的指针。一个普通的指针可以告诉你数据存储的位置,哈希指针不但可以告诉你数据存储的位置,并且还可以给你一种方式,让你验证数据没有被篡改过

2) 区块链

如图所示,我们通过哈希指针构建一个链表,我们将这个数据结构称为区块链(block chain)。在普通链表中有一系列区块,每个区块既有数据也有一个指向上一个区块的指针。而在区块链中,上一个区块指针被置换为哈希指针。因此,每个区块不仅能告诉我们上一个区块的值在哪里,还包含了该值的摘要,使我们能够验证那个值没有改变。我们存储链表头部(the head of list),即一个普通的哈希指针指向最近使用的数据区块。其中存储链表头部说明的是:我们只把头部信息取哈希链接在一起。

这样做的作用是:如果想要篡改区块链中任意地方的数据,为了保证整个内容一致,他需要篡改所有的哈希指针直至最开始的地方。他最终将碰到障碍,因为他不能篡改链表头部的指针。这样,我们便知道,仅通过记住一个哈希指针,我们就基本记住了整个链表的防篡改哈希值。因此,我们可以搭建一个包含很多区块的区块链网络,链表头部的哈希指针被称作创世区块 (genesis block)。

3) Merkle tree

使用哈希指针的二叉树也叫作Merkle trees,以其发明者Ralph Merkle的名字命名。如图所示,假设我们有很多包含数据的区块,这些区块就构成了树的叶子(节点)。我们将这些数据区块两两分组,然后为每一组建立一个有两个哈希指针的数据结构,每个指针对应一个区块,这些数据结构就构成了树的下一个层次。我们轮流将这些区块组两两分组,为每一组建立一个包含每个区块组哈希指针的新的数据结构。以此类推,直到我们得到一个单一区块,即树根节点。

在比特币中,这些数据块存的就是交易信息。

如上所述,我们要记住树最前面的哈希指针。我们现在可以通过哈希指针回溯到列表中的任何位置,这让我们能保证数据确实未经篡改,正如我们在区块链见过的一样,如果篡改了树底部的一些数据区块,会导致上一层的哈希指针不匹配,即使他继续篡改这个区块,改动数据行为将最终传递到树的顶端,而此时,将不能篡改我们存储的哈希指针。因此,同样地仅仅通过记住顶部的哈希指针,任何企图篡改任何数据的行为都会被检测到。

4) 隶属证明

与我们之前建立的区块链不同,梅克尔树的另一个特点是它可以实现简洁的隶属证明(Proof of membership)。假设某人想要证明某个数据区块隶属于Merkle tree。同样地,我们只记住树根节点,然后他需要展示给我们数据块信息,以及从该数据区块通向树根节点的那些区块,我们可以忽略树的其余部分,这样做是因为这些区块已经足够让我们验证通往树根节点过程中所有的哈希值。

5) 比特币区块代码实现

有了上面的知识,现在我们可以来分析比特币中数据结构的代码实现了,首先,我们先看区块头信息:

class CBlockHeader
{
public:
    // header
    int32_t nVersion;           // 版本号
    uint256 hashPrevBlock;      // 前一块的hash指针
    uint256 hashMerkleRoot;     // Merkle Tree hash
    uint32_t nTime;             // 产生时间
    uint32_t nBits;             // 挖矿难度 可以这么理解 nBit越小难度越大
    uint32_t nNonce;            // 挖矿调整的nonce
    // 下面的代码省略
}

区块的数据信息:

class CBlock : public CBlockHeader
{
public:
    // network and disk
    // 区块块身,保存交易信息 这里将会取一个整体哈希存在hashMerkleRoot
    std::vector<CTransactionRef> vtx;
    // 下面的代码省略
}

二、比特币中的挖矿

1) 挖矿的流程

在比特币协议中,所有的交易信息都记录在区块链上。所谓的挖矿就是争夺记账权(可以获得出块奖励,和一些小额的交易费)。假设A要向B转账,那么A首先要知道B的公钥,然后使用A的私钥签名向整个区块链网络发布转账交易,这条交易将会被其他的节点收到并记录到Merkle tree中(若本次交易合法),这些节点互相争夺记账权,当一个争夺成功,该条交易就会被添加到区块链上(这里实际上是可能,你的交易信息不一定能被所有的其他节点收到,但是没关系,你的交易将会记录到接下来的区块上),而B则可以使用上面的隶属证明方法进行验证。

那么,什么叫做争夺记账权成功呢?比特币中的争夺记账权行为实际上是指:调整区块头中nNonce属性,使整个区块头的哈希值小于一个target预值,这个预值实际上就是通过nBits计算得来,关于nBits如何计算机出一个target值,可以参考官方给出的文档,也就是说,谁先计算出这个哈希值谁就得到了记账权。

这里有必要说明一下,每个节点参加计算的交易信息的集合可能是不一样的(因为网络延迟,或者一些恶意节点),也就是说,区块头的hashMerkleRoot值可能是不一样的,这就意味着,每个节点最终算出来的哈希值可能是不一样的,不过没有关系,只要某一个节点算出了最终的哈希值,其他节点很容易验算这个哈希值是否正确和交易集合的合法性,若所有的验算都正确,这个区块的就会永久加入到区块链上(后面我们将讨论其他的情况),所有的交易信息都永久的生效了,其他节点将接着这一区块后计算新的区块。

2) 区块奖励

比特币存在的最初4年,区块奖励金额为50个比特币。但每生成210000个区块块,金额就会减半。这是一个等比数列,你可能知道数列的总和是有上限的。最终一共是21000000个比特币。

3) 交易费

比特币的第二个奖励机制称为交易费。任何交易的制造者都可以选择让交易输出值比输入值小。第一个创建区块把交易放进区块链的人可以取得这个差额,作为交易费。如果你是一个节点,正在创建一个包含200笔交易的区块,那么这200笔交易的交易费将会被付到你放在区块内的那个地址。这些交易费现在是完全自愿的,但是我们可以预见,随着区块奖励逐渐发完,交易费会变得越来越重要,几乎是必需的,因为用户需要通过交易费来保障合理的服务质量。从某种程度上来说,这已经开始发生。但目前还不清楚这个系统会如何演变——这取决于还并不完善的博弈论的研究与发展。这也是比特币一个很有趣的研究领域。

4) 挖矿实现

整个挖矿的过程分为三步,首先需要初始化区块:

std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn)
{
    // Step.1 初始化区块时间等信息
    int64_t nTimeStart = GetTimeMicros();

    resetBlock();

    pblocktemplate.reset(new CBlockTemplate());

    if(!pblocktemplate.get())
        return nullptr;
    pblock = &pblocktemplate->block; // pointer for convenience

    // Step.2 添加一个空的出块奖励到第一个交易(Merkle Tree)
    // 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();

    // Step.3 添加其他的交易信息
    // 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).
    // Note that the mempool would accept transactions with witness data before
    // IsWitnessEnabled, but we would only ever mine blocks after IsWitnessEnabled
    // unless there is a massive block reorganization with the witness softfork
    // not activated.
    // 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());

    int nPackagesSelected = 0;
    int nDescendantsUpdated = 0;
    addPackageTxs(nPackagesSelected, nDescendantsUpdated);

    int64_t nTime1 = GetTimeMicros();

    m_last_block_num_txs = nBlockTx;
    m_last_block_weight = nBlockWeight;

    // Step.4 修改出块奖励和交易费到第一个交易
    // 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);

    // Step.5 填充区块头的其他字段
    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]);

    BlockValidationState 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);
}

该过程大致分为5步:

  • Step.1 初始化区块
  • Step.2 添加一个空的出块奖励到第一个交易(Merkle Tree)
  • Step.3 添加其他的交易信息
  • Step.4 修改出块奖励和交易费到第一个交易
  • Step.5 填充区块头的其他字段

接下来就是一直尝试修改nNonce使整个区块头满足要求:

while (nMaxTries > 0 && pblock->nNonce < std::numeric_limits<uint32_t>::max() &&
       !CheckProofOfWork(pblock->GetHash(), pblock->nBits, Params().GetConsensus()) &&
       !ShutdownRequested()) {
        ++pblock->nNonce;
        --nMaxTries;
}

当计算出了一个满足条件的哈希值后,就是最后一步:调用如下方法向网络中发布这个区块

static UniValue submitblock(const JSONRPCRequest& request)

当然,我们隐藏了其中的一些异常情况的处理,因为这样更容易让人理解。

5) 挖矿难度

另一方面,比特币挖矿的难度每隔2016个区块会进行一些调整,使其每隔10分钟左右出一个区块,这样做的目的将在后面进行说明。

unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, int64_t nFirstBlockTime, const Consensus::Params& params)
{
    if (params.fPowNoRetargeting)
        return pindexLast->nBits;

    // 限制调整
    int64_t nActualTimespan = pindexLast->GetBlockTime() - nFirstBlockTime;
    if (nActualTimespan < params.nPowTargetTimespan/4)
        nActualTimespan = params.nPowTargetTimespan/4;
    if (nActualTimespan > params.nPowTargetTimespan*4)
        nActualTimespan = params.nPowTargetTimespan*4;

    // arith_uint256 256-bit unsigned big integer.
    const arith_uint256 bnPowLimit = UintToArith256(params.powLimit);
    arith_uint256 bnNew;
    bnNew.SetCompact(pindexLast->nBits); // 上一个的区块的nBit
    bnNew *= nActualTimespan;               // 实际耗费时间
    // 意味着:新的nBits = ( 实际时间/目标时间 ) * 原来的nBits
    // 但是nBits越小,目标预值就越小。挖矿难度就越大,这样就起到了调整难度的作用
    bnNew /= params.nPowTargetTimespan;     // 目标预值
    // 界限
    if (bnNew > bnPowLimit)
        bnNew = bnPowLimit;

    return bnNew.GetCompact();
}

通过阅读上面的调整nBits的代码,我们很容易知道系统是如何调整挖矿难度的。

你也许会问,当算力一直增加的时候,仅仅是调节nBits显然是不够的了(因为它只有2的32次方种可能),那么如何进一步提升挖矿难度呢?

当调整nNonce的值不足以支撑起更高挖矿难度时,我们可以调整第一交易的出块奖励的Input(每一个交易都有一个input和output,input则说明比特币的来源(这里是出块奖励,所以可以任意),output说明比特币转账的目标),进一步调整挖矿难度。就像如下的比特币代码:

void IncrementExtraNonce(CBlock* pblock, const CBlockIndex* pindexPrev, unsigned int& nExtraNonce)
{
    // Update nExtraNonce
    static uint256 hashPrevBlock;
    if (hashPrevBlock != pblock->hashPrevBlock)
    {
        nExtraNonce = 0;
        hashPrevBlock = pblock->hashPrevBlock;
    }
    ++nExtraNonce;
    unsigned int nHeight = pindexPrev->nHeight+1; // Height first in coinbase required for block.version=2
    CMutableTransaction txCoinbase(*pblock->vtx[0]);
    txCoinbase.vin[0].scriptSig = (CScript() << nHeight << CScriptNum(nExtraNonce));
    assert(txCoinbase.vin[0].scriptSig.size() <= 100);

    pblock->vtx[0] = MakeTransactionRef(std::move(txCoinbase));
    pblock->hashMerkleRoot = BlockMerkleRoot(*pblock);
}

6) 挖矿设备

挖矿设备的演化趋势越来越专业化,主要经过三次演化

  • CPU 挖矿
  • GPU 挖矿
  • 矿机 ASIC(Application Specific Integrated Circuit)芯片

7) 矿池

设想一下作为单个矿工。假设你花了辛苦赚来的6 000美元买了一台全新闪亮的比特币矿机,你所期望的性能是平均每14个月会找到一个有效区块(在2015年早期一个区块的奖励价值在10000美元)。

考虑到电费和其他运营成本,矿机的平均收入期望值应该是每个月400美元。如果可以确定每个月都能获得400美元,那么购买一台矿机是合理的投资。但是别忘了,挖矿是一个随机过程,你不知道什么时候可以发现下一个有效区块。在找到有效区块之前,什么都赚不到。

历史上当小商人遇到大风险的时候,他们会自发组建一个互助保险公司来降低风险。比如,农夫会自发地聚在一起形成一个协议,如果任何一个个体农夫的谷仓不小心被烧掉了,那么其他的农夫可以把他们的利润拿来和这个不幸的农夫分享。那么对于比特币的小矿工是否也可以用类似的方式来降低风险呢?

矿池应运而生——矿池就是一个比特币矿工互相之间的保险。一组矿工可以形成一个矿池共同进行挖矿,并指定一个币基接受人。这个接受人就是矿池管理员。所以不管是谁最终发现了一个有效区块,矿池管理员将会收到这个区块的奖励,继而根据每个参与者所贡献的工作量按比例分配给所有矿池的参与者。当然,矿池管理员可能从中分一部分来作为矿池管理服务的收入。

假定每个人都信任这个矿池管理员,这样的分配安排可以极大地降低矿工成功寻找有效区块的概率波动。但是矿池管理员如何知道矿池里每个成员实际上到底贡献了多少工作量呢?同时他又是如何去分发收入的呢?很显然,矿池管理员不希望是靠每个成员的申明,因为他们可能会虚报自己的工作量。

对于这个问题,我们有一个简洁的解决办法。矿工可以通过输出挖矿工分(mining shares)来证明他的工作量,工分就是那些接近有效区块的区块。比如目标值是个前面67位是零的数字,输出的哈希值必须要低于这个目标才算有效。在寻找这个哈希值的过程中,矿工可能找到其他一些区块,它们的哈希也有许多零,但达不到67个。矿工可以用这些区块来证明他们确实在工作,一个合格的工分可能要求40~50个零,取决于矿工所加入的矿池的要求。

在这个模式里,管理员会对每一个超过特定区块难度的工分发放固定的奖励分红。在这个模式里,矿工在发送工分之后,管理员马上就会对其支付奖励,而不需要等到整个矿池发现一个有效区块。

三、挖矿中的一些常见问题

1) 当两个节点同时计算出下一个区块

考虑这样一种情况,区块链中有两个区块,区块1和区块2,A和B同时挖出了一个区块区块3和区块4,现在的情况如下图所示:

此时区块链上产生了分叉,其他节点会随机的(或者基于特定算法)选择区块3和区块4继续进行挖矿,也就是说,分叉将会持续一段时间,直到其中一个区块成为最长合法链,另一个分叉将会被忽略(意味着,短的分叉的交易将会回滚,所以一般情况下,我们判断一个交易是否存入区块中,一般要多等几个区块产生)。

如上图所示,此时下面的分叉成为了最长的合法链,上面的将被忽略(诚实的节点)。实际上,你也可以从上面的区块开始挖矿,只要你能超过下面分叉的增长,显然,只要上面的算力没有超过51%,这基本上是不可能的。

2) Selfish Mining

考虑下面的情况,区块链中有两个区块,区块1和区块2,A基于区块2挖出了一个区块区块4并在区块4的基础上挖出了区块5,而其他节点还在基于区块2挖下一个区块:

此时若其他节点发布了区块2后面的区块6,A立刻发布区块4和区块5,区块6立刻就无效了,这样A就浪费了其他节点的工作量,不过这样风险也相当大(因为你可能会被其他节点超过)。

3) 挖矿的速度太快

这个问题相当简单,如果挖矿的速度不进行调整必然会造成过多的分叉。

4) 需要等待多少个区块交易才不会回滚

这个问题可以参看这篇论文

四、阅读更多

分类: 计算机科学