Proof-of-Work 工作量证明

区块里有一个非常关键的点,就是节点必须执行足够多且困难的运算才能将数据新增在区块中。这一困难的运算保证了区块链安全、一致。而为了奖励这一运算,该节点会获得数字货币(如比特币)的奖励(从运算到收到奖励的过程,也叫作挖矿)。 

这一机制和现实生活中也是相似的:人们辛苦工作获取报酬来维持生活,在区块链中,链络中的参与者(比如矿工)辛苦运算来维系这个区块链网络,不断增加新的区块到链络中,然后获取回报。正是因为这些运算,新的区块基于安全的方式加到区块链中,保证了区块链数据库的稳定。 

是不是发现了什么问题呢?大家都在计算,凭什么怎么证明你做的运算就是对的,且是你的。 

努力工作并证明(do hard work and prove),这一机制被称为工作量证明。需要多努力呢,需要大量计算机资源,即使使用高速计算机也不能做得快多少。而且,随着时间的推移,难度会越来越大,因为要保证每小时有6个区块的诞生,越到后面,区块越来越少,要保证这个速率,只能运算更多,提高难度。在比特币中,运算的目标是计算出一串符合要求的hash值。而这个hash就是证明。所以说,找到证明(符合要求的hash值)才是实际意义上的工作。 

工作量证明还有一个重要知识点。也即工作困难,而证明容易。因为如果你的工作困难,而证明也困难,那么你的工作在圈子效率意义就不大,对于需要提供给别人证明的工作,别人证明起来越简单就越好。

Hash 哈希

Hash运算是区块链最主要使用的工作算法。哈希运算是指给特殊数据计算一串hash字符的过程。对于一笔数据而言,它的hash值是唯一的。hash函数就是可以把任意大小的数据计算出指定大小hash值的函数。 

Hash运算有以下几个主要的特点: 

1. 原始数据不能从hash值中逆向计算得到 

2. 确定的数据只有一个hash值且这个hash值是唯一的 

3. 改变数据的任一byte都会造成hash值变动

Hash运算广泛应用于数据一致性的验证。很多线上软件商店都会把软件包的hash值公开,用户下载后自行计算hash值后验证和供应商的是否一致,就可判断软件是否被篡改过。 

在区块链中,hash也是用于保证数据的一致性。区块的数据,还有区块的前一区块的hash值也会被用于计算本区块的hash值,这样就保证每个区块是不可变:如果有人要改动自己区块的hash值,那么连他后面的区块hash也要跟着改,这显然是不可能的或者极其困难的(要说服不是自己的区块一同更改很困难)。

Hashcash 哈希现金

比特币中的工作证明使用的是Hashcash技术,起初,这一算法开发出来就是用于防止垃圾电子邮件。它的工作主要有以下几个步骤: 

1. 获取公开的信息,比如邮件的收件人地址或者比特币的头部信息 

2. 增加一个起始值为0的计数器 

3. 计算出第1步中的信息+计数器值组合的hash值 

4. 按规则检测hash值是否满足需求(一般是指定前20位是0) 

1. 满足 

2. 不满足则重复3-4步骤 

这个算法看上去比较暴力:改变计数器值,计算新的hash,检测,增加计数器值,计算新的hash…,所以这个算法比较昂贵。 

邮件发送者预备好邮件头部信息然后附加用于计算随机数字计数值。然后计算160-bit长的hash头,如果前20bits 

现在进一步分析区块链hash运算的要求。在原始的hashcash实现中,必须根据头信息算出前20位为0的hash值。而在比特币中,这一规则则是根据时间的推移变动的,因为比特币的设计就是10分钟出一块新区块,即使计算机算力提升或者更多的矿工加入挖矿行列中也不会改变,也就是说,算出hash值,会越来越困难。 

下面演示这一算法,和上面第一张图一样使用“I like donuts”作为数据,再在数据后加面附加计数器,然后使用SHA256算法找出前面6位为0的hash值。

而ca07ca就是不停运算找到的计数器值,转换成10进制就是13240266,换言之大概执行了13240266次SHA256运算才找到符合条件的值。

实现

上面花了点篇幅介绍了工作量证明的原理。现在我们用Golang来实现。先定24位0的作为挖矿的难度: 

const targetBits = 24 

注:在比特币挖矿中,头部中的target bits存储该区块的挖矿难度,但是上面说过随着时间推移难度越来越大,所以这个target大小是会变的,这里不实现target的适配算法,这不影响我们理解挖矿。现在只定义一个常量作为全局的难度。 

当然,24也是比较随意的,我们只用在内存中占用少于256bits的的空间。差异也要大些,但是也不要太大,太大了就就很难找出来一个合规的hash。 

然后定义ProofOfWork结构:

type ProofOfWork struct {
block *Block
target *big.Int
}
func NewProofOfWork(b *Block) *ProofOfWork {
target := big.NewInt(1)
target.Lsh(target, uint(256-targetBits))
pow := &ProofOfWork{b, target}
return pow
}

ProofOfWork 有“block”和“target”两个成员。“target”就是上面段落中描述的hashcash的规则信息。使用big.Int是因为要把hash转成大整数,然后检测是否比target要小。

NewProofOfWork 函数负责初始化target,将1往左偏移(256-targetBits)位。256是我们要使用的SHA-256标准的hash值长度。转换成16进制就是:

0x10000000000000000000000000000000000000000000000000000000000

这会占用29位大小的内存空间。

现在准备创建hash的函数:

func (pow *ProofOfWork) prepareData(nonce int) []byte {
data := bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.Data,
IntToHex(pow.block.Timestamp),
IntToHex(int64(targetBits)),
IntToHex(int64(nonce)),
},
[]byte{},
)
return data
}

这段代码做了简化,直接把block的信息和target、nonce合并在一起。nonce就是Hashcash中的counter,nonce(现时标志)是加密的术语。 

准备工作都OK了,现在实现PoW的核心算法:

func (pow *ProofOfWork) Run() (int, []byte) {
var hashInt big.Int
var hash [32]byte
nonce := 0
fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)
for nonce < maxNonce {
data := pow.prepareData(nonce)
hash = sha256.Sum256(data)
fmt.Printf("\r%x", hash)
hashInt.SetBytes(hash[:])
if hashInt.Cmp(pow.target) == -1 {
break
} else {
nonce++
}
}
fmt.Print("\n\n")
return nonce, hash[:]
}

hashInt是hash值的int形式,nonce是计数器。然后执行math.MaxInt64次循环直到找符合target的hash值。为什么是math.MaxInt64次,其实这个例子中是不用的考虑这么大的,因为我们示例中的PoW太小了以致于还不会造成溢出,只是编程上要考虑一下,当心为上。 

循环体内工作主要是: 

1. 准备块数据 

2. 计算SHA-256值 

3. 转成big int 

4. 与target比较 

将上一篇中的NewBlock方法改造一下,扔掉SetHash方法:

func NewBlock(data string, prevBlockHash []byte) *Block {
block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, 0}
pow := NewProofOfWork(block)
nonce, hash := pow.Run()
block.Hash = hash[:]
block.Nonce = nonce
return block
}

新增了nonce作为Block的特性。nonce作为证明是必须要带的。现在Block结构如下:

type Block struct {
Timestamp int64
Data []byte
PrevBlockHash []byte
Hash []byte
Nonce int
}

然后执行 go run *.go

start: 2018-03-07 14:43:31.691959 +0800 CST m=+0.000721510
Mining the block containing "Genesis Block"
0000006f3387b588739cbcfe2cce521fcce27d4306776039e02c2904b116ab9a
end: 2018-03-07 14:44:32.488829 +0800 CST m=+60.798522580
elapsed time: 1m0.797798933s
start: 2018-03-07 14:44:32.489057 +0800 CST m=+60.798750578
Mining the block containing "Send 1 BTC to Ivan"
000000e9d5a266faa6a86f56a36ea09212ecad28e524a8b0599589fd5b800d13
end: 2018-03-07 14:46:32.996032 +0800 CST m=+181.307571128
elapsed time: 2m0.508818203s
start: 2018-03-07 14:46:32.996498 +0800 CST m=+181.308036527
Mining the block containing "Send 2 more BTC to Ivan"
0000001927685501c59f28c0bda3fdd0472e88d6eec3822b0ab98e5b1c28c676
end: 2018-03-07 14:46:53.90008 +0800 CST m=+202.211938702
elapsed time: 20.903900066s

可以看到生成了前6位都是0的hash字符串,因为是16进制的,就是24一位,共4*6=24位,也就是我们设置的targetBits。从时间上可以看到,计算出hashcash的时间有一定的随机性,多着2分,少则20秒。 

现在还需要去验证是否是正确的。

func (pow *ProofOfWork) Validate() bool {
var hashInt big.Int
data := pow.prepareData(pow.block.Nonce)
hash := sha256.Sum256(data)
hashInt.SetBytes(hash[:])
isValid := hashInt.Cmp(pow.target) == -1
return isValid
}

可以看到,nonce在验证是要用到的,告诉对方也用我们计算出来的数据进行二次计算,如果对方计算的符合要求,说明我们的计算合法。 

把验证方法加到main函数中:

func main() {
...
for _, block := range bc.blocks {
...
pow := NewProofOfWork(block)
fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
fmt.Println()
}
}

结果:

Prev. hash:
Data: Genesis Block
Hash: 0000006f3387b588739cbcfe2cce521fcce27d4306776039e02c2904b116ab9a
PoW: true
Prev. hash: 0000006f3387b588739cbcfe2cce521fcce27d4306776039e02c2904b116ab9a
Data: Send 1 BTC to Ivan
Hash: 000000e9d5a266faa6a86f56a36ea09212ecad28e524a8b0599589fd5b800d13
PoW: true
Prev. hash: 000000e9d5a266faa6a86f56a36ea09212ecad28e524a8b0599589fd5b800d13
Data: Send 2 more BTC to Ivan
Hash: 0000001927685501c59f28c0bda3fdd0472e88d6eec3822b0ab98e5b1c28c676
PoW: true

本章总结 

本章我们的区块链进一步接近实际的结构:增加了计算难度,这意味着挖矿成为可能。不过还是欠缺了一些特性,比如没有把计算出来的数据持久化,没有钱包(wallet)、地址、交易,以及实现一致性机制。接下来的几篇abc中,我们会持续完善。


  • 学院Go语言视频主页​​

  • 扫码获取海量视频及源码   QQ群:721929980