比特币源码分析--深入理解区块链11.共识机制:工作量证明PoW以及实现原理

        工作量证明(Proof-of-Work,PoW)是一种对应服务与资源滥用、或是拒绝服务攻击的经济对策。一般要求用户进行一些耗时适当的复杂运算,并且答案能被服务方快速验算,以此耗用的时间、设备与能源做担保成本,以确保服务与资源是被真正的需求所使用。此概念最早由Cynthia Dwork和Moni Naor于1993年的学术论文提出,而工作量证明一词则是在1999年由Markus Jakobsson与Ari Juels.所发表。现时此技术成为了加密货币的主流共识机制之一,而比特币是采用PoW的先驱。

        我们常听到的比特币挖矿就是通过计算机来计算符合特定要求的哈希值(hash256),并不是解一个方程。挖矿计算量大的原因并不是因为计算复杂度高,而是要尝试非常多次,才能确定一个符合要求的哈希值。因此运算量大小是衡量比特币矿工挖矿工作量大小的依据。熟悉hash运算的都知道,这种运算是大量的二进制的基本运算,因此非常适合直接使用硬件的指令来实现,全球才有很多的矿机生产商生产专门的硬件用于挖矿,连Intel也要推出专门用于挖矿的CPU。PoW具有以下优缺点:

优点:

  • 架构简明扼要、有效可靠。
  • 由于要获得多数节点承认,那攻击者必须投入超过总体一半的运算量(51%攻击),才能保证篡改结果。这使得攻击成功的成本变得非常高昂,难以实现。
  • 某种程度上是公平的,你投入越多的算力,你获得打包权的几率也等比增加。

缺点:

  • 非常浪费能源。投入在一种加密货币上的能源,可能会超过一个小型国家的总使用量。
  • 由于加密货币在世界上已成为一种投资标的,所以技术人员开发出了由ASIC组成的特制计算设备(矿机),垄断算力。这与加密货币的去中心化思想背道而驰。也因此,后期开发的加密货币有针对抗ASIC的算法设计,例如以太坊采用的Ethash(Dagger-Hashimoto)算法。
  • 后期开发的加密货币陆续使用了POS机制(例如以太坊)或DPOS机制。

工作量证明的特征值

        工作量证明最常用的技术原理是哈希函数(也叫散列函数)。由于输入散列函数h的任意值n,会对应到一个h(n)结果,而只要变动一个比特,就会引起雪崩效应,所以几乎无法从 h(n)反推回n,因此借由指定查找h(n)的特征,让用户进行大量的穷举运算,就可以达成工作量证明。根据雪崩效应:它是指当输入发生最微小的改变(例如,反转一个二进制位)时,也会导致输出的不可区分性改变(输出中每个二进制位有50%的概率发生反转)。也就是说散列函数h(n)的结果是符合雪崩效应的。那么当我们要求散列的结果符合某些特征时,这明显有悖于雪崩效应,这时难度就会上升。比特币就是通过指定工作量特征值前面的零的个数来调整工作量大小(或者难度)。

现在我们假设定h(n)的结果只有4个bit位。因此这4个bit位的可能结果如下:

二进制位

0000

扫描二维码关注公众号,回复: 14216412 查看本文章

0

0001

1

0010

2

0011

3

0100

4

0101

5

0110

6

0111

7

1000

8

1001

9

1010

10

1011

11

1100

12

1101

13

1110

14

1111

15

若我们规定前一位是0,结果有8个,概率是

若我们规定前两位是0,结果有4个,概率是

若我们规定前三位是0,结果有2个,概率是

若我们规定前四位是0,结果有1个,概率是

可以看出前面0的个数多少决定了运算次数。0的个数越多运算次数也越多。若要一个h(n)的结果前面有m个0 bit位,需要的哈希运算次数(工作量)是:

比特币的哈希值采用的是Hash256,有256位长度,规定工作量的特征值里面前面至少有32个0(4个字节),而后面的224个位的0的个数是变化的。

如下图:

这里的target就是Bitcoin可以调节的工作量证明的特征值。下面就是比特币主网的工作量证明的最小值:

该值前面有8个十六进制0,一个代表4位,总共有32位。

        上面我们讲到挖矿就是计算出符合特定要求的哈希值,该特征值是以一个大数(Big Integer)形式表示的。熟悉计算机编程的都知道,大多数计算机能处理的基本数据类型最长也只有8个字节(64位),可以表示多达= 18,446,744,073,709,551,616个数,这已经足够大,对于大多数应用已经足够。而比特币的工作量特征值是一个长度是256位,它是天文数字,大到不能用基本的类型处理,所以256位长度在编程时必须使用专门的类来实现。为什么是256位,因为使用了Hash256算法,哈希值与工作量特征值位长保持一致。如果使用十六进制来表示该特征值的话是64个字符长度

        比特币要求矿工计算的哈希结果要符合工作量特征值要求的含义是:计算结果并不是与特征值相等,而是要求小于或等于特征值。为什么是小于或等于特征值呢? 是因为要符合前面8个0的特征的话,计算出来的数必须要求小于或等于PowLimit。这说明前面0的个数越多,计算量越大。也可以说数字越小,计算量越大。所以要求矿工计算出来的哈希值结果是小于或等于PowLimit。注意这里是基于工作量的比较,而不是纯粹数字大小的比较。

          PowLimit只是限定了工作量要求的最小值,实际上比特币的共识系统会自动调整工作量特征值target,它并不是一成不变的,随着时间的推移工作量会动态变化。Target的长度有32 – 4 = 28字节,在程序里无法直接使用基本的数据类型来表示。因此比特币在设计上为了节省存储空间,采用了紧凑(comapct)格式,使用了一个无符号的32位(uint32_t)类型来表示。在每个区块头信息里面,其中有一个名叫nBits字段,它就是工作量证明特征值的紧凑格式,为了说明nBits这种存储格式,我们先了解一下浮点数的二进制表示。

浮点数的二进制表示法

我们先回忆一下单精度(32位,c++一般使用float类型)浮点数的二进制表示。

符号位sign:1表示负数,0表示正数

指数位exponent:也叫阶码,共8位,可表示范围在,也就是的范围。为什么会有负数,是因为指数可以为负数。

尾数mantissa:共23位。

在这里关于如何将一个浮点数数转换为二进制表示这里就不详细说明。

IEEE浮点数表示标准是这样的:

其中,  尾数在使用科学计数法表示之后,尾数的范围是:



Bias示偏置量,32位的单精度浮点数的偏置量:

        上述的e是将浮点数表示为二进制时的阶码位数加上偏置量后的长度。当浮点数的整数部分为零时,只有小数部分时,在使用IEEE浮点数表示标准时,指数E为负数。说明 。 而 , n是阶码(尚未加上偏置量的值)。比如十进制0.5用二进制表是0.1, 可以表示为,阶码相当于-1。因此阶码是负数时表明该浮点数< 1。

使用nBits来表示工作量特征值

        Bitcoin中的nBits使用了一个无符号的32位数来表示28字节的工作量特征值。称之为“紧凑( compact)”表示方法。前面它使用了类似浮点数的二进制表示方法。注意这里仅仅只是类似,并不完全一样。

nBits的数据格式

在实际使用时为了方便把28字节扩展到32字节,前面以0补足。因此

用nBits实现一个无符号的256位数(uint256)时可用下列公式表示:

注意这指数的底数不再是2,而是256。在实际换算过程中,可使用:

        其中N是一个使用了compact表示法的无符号256位数(Bitcoin中对应的是arith_uint256类型。注意在Bitcoin还有一个uint256类型,它仅仅表示一个32字节的数组,不支持数学运算和符号位。而arith_uint256是一个可进行数学运算的Big  Integer,支持加、减、乘、除等运算)。

其中尾数mantissa最大可表示0x007FFFFF。

通过下列方法从一个无符号的32位数nBits中解析出这种格式:

指数:

尾数:

如果exponent少于3,按照上面的公式,指数是负数,因此最后的结果是该uint256表示的是一个整数部分是0,只有小数部分的数字。

根据

        上面的公式表示的意思是尾数M乘以2的n次方相当将M左移n位。M除以2的n次方相当于将M右移n位(这里的除法只讨论定点数部分,就是整数部分)。因此当exponent < 3时上述公式的指数部分将现负数,这时就需要使用右移运算,其结果表明该数 < 1(这里说的结果是除掉符号位的讨论),且只有小数部分。由于移位运算必须是一个正整数,因此使用exponent – 3的绝对数,或使用3 – exponent来获得移位运算时的位数n。

时向右移:

时向左移:

Bitcoin代码如下:

https://github.com/bitcoin/bitcoin/blob/master/src/arith_uint256.cpp

arith_uint256& arith_uint256::SetCompact(uint32_t nCompact, bool* 
pfNegative, bool* pfOverflow)
{
    int nSize = nCompact >> 24;
    uint32_t nWord = nCompact & 0x007fffff;
    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;
}

上述代码nSize表示指数位数n,nWord就是尾数mantissa。nCompact相当于nBits。

Compact中的负数判断:

符合尾数不等于0且符号位不等于0

Compact中的是否溢出:

溢出分为左溢出和右溢出:

左溢出是左移位时,最低位的字节或有效的尾数超过了最高字节范围,arith_uint256有32个字节,共256位,假设最高位字节序号是31,最低位序号是0,如下图所示:

当左移符合下列条件才算溢出:

  1. 尾数不等于0
  2. 且同时满足下列条件之一:
  •    没有超出范围,因此当 时肯定发生溢出。
  • 尾数M最大可表示为0x007FFFFF,换算成二进制是

0111 1111 1111 1111 1111 1111(23个有效位)。

因此当它往左移动31个字节时,将丢弃除了最低一个字节以外的所有字节,也就是丢弃的部分。其结果是
1111 1111 0000......00000(共256位), 因此这种情况也算溢出。
如下图所示:

  • 因此当它往左移动30个字节时,将丢弃除了最低两个字节以外的所有字节,也就是丢弃的部分。其结果是
    1111 1111 1111 1111 0000......00000(256位),因此也算溢出。
    如下图所示:

因此当左移动少于30位时,溢出不会发生。

        右移时不会发生溢出,因为32位的尾数最多右移3个字节(3- n, 当n=0时最大),移位之后的长度最多也就是56位,远没有超过256位长度的限额。

Bitcoin中的代码:

    if (pfNegative)
        *pfNegative = nWord != 0 && (nCompact & 0x00800000) != 0;
    if (pfOverflow)
        *pfOverflow = nWord != 0 && ((nSize > 34) ||
                                     (nWord > 0xff && nSize > 33) ||
                                     (nWord > 0xffff && nSize > 32));

工作量证明难度调整

        在上面我们介绍了要增加工作量,就可以在特征值前面增加0的个数。从而达到提升难度的目的。Bitcoin系统每间隔一段时间会自动调整一次目标区块的计算难度,在bitcoin/chainparams.cpp at master · bitcoin/bitcoin · GitHub代码里面是这样设定主网参数的:

consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks consensus.nPowTargetSpacing = 10 * 60;

nPowTargetTimespan是调整需要满足的时间跨度,以秒为单位,这里设定的是2周。

nPowTargetSpacing是在该时间跨度内每隔多长时间计算一次Hash,也就是说每隔多长时间计算出一个新的区块的hash值,这个相当于矿工挖矿时算出新区块的间隔时间。根据上述参数我们可以确定,每隔2016个块就需要重新调整一次工作量难度。

通过代码我们看看如何调正目标工作量难度的:

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
    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
    int nHeightFirst = pindexLast->nHeight - (params.DifficultyAdjustmentInterval()-1);
    assert(nHeightFirst >= 0);
    const CBlockIndex* pindexFirst = pindexLast->GetAncestor(nHeightFirst);
    assert(pindexFirst);

    return CalculateNextWorkRequired(pindexLast, pindexFirst->GetBlockTime(), params);
}

代码说明:

unsigned int nProofOfWorkLimit = UintToArith256(params.powLimit).GetCompact();

获取当前网络工作量证明的限制,可以理解为必须满足的最小工作量,这里的最小不是数值的绝对大小,而是指计算符合该限制值的需要的计算难度或工作量。该限制通过上述说明的紧凑格式存入一个uint256类型大数类型,在这里通过GetCompact将其还原成一个无符号整数。

if ((pindexLast->nHeight+1) % params.DifficultyAdjustmentInterval() != 0)

说明只有当区块高度height满足下列条件是才调整工作量证明的特征值:



,  

根据上述公式可以计算出,只有当区块高度等于下列值时会自动调整工作量证明的特征值,

这里有一个例外,就是对于Bitcoin的测试网络,当相邻两个区块的时间间隔大于2倍nPowTargetSpacing(20分钟)时,可以将难度降低到工作量证明的最小,就是上述代码中的nProofOfWorkLimit。

    int nHeightFirst = pindexLast->nHeight - (params.DifficultyAdjustmentInterval()-1);

    assert(nHeightFirst >= 0);

    const CBlockIndex* pindexFirst = pindexLast->GetAncestor(nHeightFirst);

    assert(pindexFirst);

该代码是指当区块高度height等于上面数字时,也就是说即将满足需要调整工作量证明特征值需要的区块高度时,计算当前工作量证明特征值起始和结束区块高度。因此nHeightFirst的值满足:

对应关系是:
 

我们再看看如何通过CalculateNextWorkRequired计算下一区块的工作量证明特征值:
https://github.com/bitcoin/bitcoin/blob/master/src/pow.cpp

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

        这里fPowNoRetargeting标记指示不需要调整工作量,Bitcoin在回归测试模式(RegTest)下是不需要调整工作量难度的。

        通过计算当前工作量证明所包括的起始和结束区块的实际时间跨度,当实际时间跨度少于当前网络规定的时间跨度的四分之一时,实际时间跨度设置为当前网络规定的时间跨度的四分之一;当实际时间跨度大于当前网络规定的时间跨度的四倍时,实际时间跨度设置为当前网络规定的时间跨度的四倍,因此实际时间跨度计算公式是:

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

从上面的代码我们不难看出计算下一个周期的工作量证明特征值bnNew。我们用公式可表示为:

        从上面公式可以看出,当实际的时间跨度大于系统规定的时间跨度时,bnNew的值的从数学上看大于上一个周期工作量证明的数值,根据上面对工作量证明的难度理解,可以看出工作量难度降低。也就是说当出块时间越来越长时,说明Bitcoin网络的算力在下降,系统会自动在下一个周期降低工作量难度。反之就会提高难度。

比特币区块中的nBits

        我们从Bitcoin主网的区块查询网址可以查询到区块高度为731994的nBits=386,529,497,该区块的产生时间是GMT+8:2022-04-15 22:41,使用紧凑型解码为十六进制工作量目标特征值是:

00000000000000000009f8d90000000000000000000000000000000000000000

可以看出前面已达到19个0,工作量难度已非常高。我们再对比高度是1区块的nBits = 486,604,799,解码后:00000000ffff0000000000000000000000000000000000000000000000000000

该值前面还是8个0,接近工作量最小值PowLimit。

如何获取区块中的工作量(哈希运算次数)

        前面提到,Bitcoin网络的工作量证明通过计算Hash值知否满足指定的特征值,我们若指定h(n)的16进制值的前8值,求n,这样统计上平均约要运行232​次散列运算,才会得到答案。极端情况下,当h(n)如果全是0(当然这种情况永远不会发生):

通过下面的公式计算区块工作量(预期计算哈希次数)

        这里的bnTarget就是上面提到的工作量证明的特征值。它保存在每个区块的nBits字段中。通过SetCompact编码成一个uint256类型的数字。由于计算2的256次方的计算量太大,可以对上述公私进行简化,上面的公式与下面的相等:

根据

(~是取反运算,无符号数相当于它与上界值的差,而256位能表示的最大值=​。所以:

区块工作量有效性检查

        Bitcoin客户端在同步区块时,系统会自动检查区块的工作量是否满足要求。每一个区块都有一个唯一的hash,该hash就是矿工根据工作量要求计算出来的。因此正常情况下它必须满足工作量要求,区块工作量检查的代码如下:

https://github.com/bitcoin/bitcoin/blob/master/src/pow.cpp

bool CheckProofOfWork(uint256 hash, unsigned int nBits, const 
Consensus::Params& params)
{
    bool fNegative;
    bool fOverflow;
    arith_uint256 bnTarget;

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

根据上面我们对于工作量特征值的解释,不难理解,区块工作量必须满同时满足两个条件:

也就是说 区块的hash值中的工作量必须大于或等于目标工作量,且目标工作量要大于或等于PowLimit。

猜你喜欢

转载自blog.csdn.net/dragon_trooquant/article/details/124213936