title: Golang实现区块链(二)—工作量证明(poW)
tags: go,blockchain
在上一篇的文章中我们实现一个简单的区块链结构。相信大家都知道比特币的挖矿,本文就将基于上篇文章来实现poW挖矿功能。
上篇文章地址:https://blog.csdn.net/yang731227/article/details/82932990
poW 工作量证明
挖矿原理
在讲poW之前我们先来讲讲以比特币为例的挖矿原理,其实说挖矿其实并不准确,我们应该称其为记账。记账是把交易记录、交易时间、账本序号、上一个Hash值等信息计算Hash打包的过程。这一过程必然需要某个计算机来实现,这类计算机我们下面统称为“节点”。因为区块链是分布式的,所以就需要很多节点,而且计算需要消耗很多资源。如果没有一点奖励机制,大家肯定都不情愿,把自己的设备作为免费节点。因此在比特币中,中本聪设定了只要完成记账的节点给予一定的比特币作为奖励,因此大家把记账形象的称为“挖矿”。
记账原理
上面的我说了,成功的完成记账后就会有比特币奖励,因此就出现大家争相记账,大家一起记账就会引起问题:出现记账不一致的问题,怎么判断你的记账是正确的。比特币系统引入工作量证明来解决这个问题,规则如下:
1.一段时间内只有一人可以记账成功
2.通过解决密码学难题(即工作量证明)竞争获得唯一记账权
3.其他节点复制记账结果那么我们通过什么来判断你完成了记账。
在进行工作量证明之前,记账的节点会做一些工作:
- 收集广播中海没有被记录的原始交易信息
- 检查交易信息中付款地址有没有足够的余额
- 验证交易是否有正确的签名
- 把验证通过的交易信息进行打包记录
- 添加一个奖励交易:给自己的地址增加特币
poW
poW全称Proof-of-Work 即工作量证明。通过记账原理我们知道,每次记账就是把上一个块的Hash值和当前块的信息一起作为原始信息进行Hash。如果仅仅这么轻松点完成了记账,相信比特币也不值钱。因此为了保证一段时间内只有一个人能完成记账,就需要提高记账难度。而且,随着时间的推移,难度会越来越大,因为要保证每小时有6个区块的诞生,越到后面,区块越来越少,要保证这个速率只能运算更多,提高难度。在比特币中,运算的目标是计算出一串符合要求的hash值。而这个hash就是证明。所以说,找到证明(符合要求的hash值)才是实际意义上的工作。
我们知道改变Hash的原始信息的任何一部分,Hash值也会随之不断的变化,因此在运算Hash时,不断的改变随机数的值,总可以找的一个随机数使的Hash的结果以若干个0开头(下文把这个过程称为猜谜),率先找到随机数的节点就获得此次记账的唯一记账权。Hash值示例:
00000017504a984ab0339f7d1517691e63099fb83d6ae9d6ebd722c25755f2cc
计算量分析
以我写这篇文件的时间来看(2018-10-4),挖一个比特币的成本需要3W多人民币。为什么成本这么高呢?我们通过比特币来简单分析下挖矿难度有多大。Hash值是由数字和大小写字母构成的字符串,每一位有62种可能性,假设任何一个字符出现的概率是均等的,那么第一位为0的概率是1/62(其他位出现什么字符先不管),理论上需要尝试62次Hash运算才会出现一次第一位为0的情况,如果前两2位为0,就得尝试62的平方次Hash运算,以n个0开头就需要尝试62的n次方次运算。
我们以Block #544228为例,它当前hash为:000 000 000 000 000 000 14d90bb35d84eaf8b5ed2ae85f42b9d37c715ab3a02b06。 可以看到它前面有24个0,那么理论上就需要尝试 64^24 ,大概需要2.230074519853062e43 次这是一个非常大的数字,需要消耗很大的资源(电能、算力),所以比特币目前来说非常难挖。
实现poW
上面花了点篇幅介绍了工作量证明的原理。现在我们根据上篇的内容进行修改,来实现poW。
定义挖矿难度
先定义20位0作为挖矿难度,我们这里的难度是全局的,并且不作改变。
ps:在实际区块链中,targetBit是变化的,挖矿是随着时间变的越来越难。
const targetBit =20
因为我们这里只做演示,所以targetBit不能太大,不然会消耗很多时间。
区块结构
相比于上篇的代码我们添加了一个新的属性 Nonce ,用于生成工作量证明的哈希。
type Block struct {
Index int64
TimeStamp int64
Data []byte
PrevBlockHash []byte
Hash []byte
Nonce int64
}
Nonce 用来保存一个随机值,poW算法中Nonce和区块其他信息一起进行Hash计算,使计算符合一定的条件,如比特币的Hash值是需要前几位为0,像000017504… 这样我们需要找出这样的一个随机数使得Hash值前四位为0.
定义poW
type ProofOfWork struct {
block *Block
target *big.Int
}
func NewProofOfWork (b *Block) *ProofOfWork {
target:= big.NewInt(1)
target.Lsh(target,uint(256-targetBit))
pow:=&ProofOfWork{b,target}
return pow
}
target这里使用了一个 大整数 ,将会用hash与target进行比较:先把哈希转换成一个大整数,然后检测它是否小于目标。
在 NewProofOfWork 函数中,我们将 big.Int 初始化为 1,然后左移 256 - 1 位。则target(目标) 的 16 进制形式为:
0x10000000000000000000000000000000000000000000000000000000000
准备数据
我们先使用IntToHex函数将int型数据转行为16进制。
func IntToHex(data int64) []byte {
buffer := new(bytes.Buffer) // 新建一个buffer
err := binary.Write(buffer, binary.BigEndian, data)
if nil != err {
log.Panicf("int to []byte failed! %v\n", err)
}
return buffer.Bytes()
}
上面说过,poW算法中Nonce和区块其他信息一起进行Hash计算,使计算符合一定的条件。现在我们就用prepareData函数将Nonce与区块其他信息合并。
func (pow *ProofOfWork)prepareData(nonce int64)[]byte{
data:=bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.Data,
IntToHex(pow.block.Index),
IntToHex(pow.block.TimeStamp),
IntToHex(int64(targetBit)),
IntToHex(int64(nonce)),
},
[]byte{},
)
return data
}
工作量证明
这段是实现poW的核心代码:
func (pow *ProofOfWork)Run()(int64 ,[]byte) {
var hashInt big.Int
var hash [32]byte
var nonce int64 =0
fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)
for {
dataBytes :=pow.prepareData(nonce) //获取准备的数据
hash =sha256.Sum256(dataBytes) //对数据进行Hash
hashInt.SetBytes(hash[:])
fmt.Printf("hash: \r%x",hash)
if pow.target.Cmp(&hashInt) ==1 { //对比hash值
break
}
nonce++ //充当计数器,同时在循环结束后也是符合要求的值
}
fmt.Printf("\n碰撞次数: %d\n", nonce)
return int64(nonce),hash[:]
}
通过Run函数我们可以看到,函数不断的循环查找符合要求的nonce值。
当target值大于hashInt 退出循环,并返回计数次数和正确hash。
循环体内工作主要是:
- 准备块数据
- 计算SHA-256值
- 转成big int
- 与target比较
创建新的区块
下面我将对对上篇文章中的NewBlock 函数进行适当的修改。
func NewBlock(index int64,data string ,prevBlockHash []byte )*Block{
block :=&Block{index,time.Now().Unix(),[]byte(data),prevBlockHash,[]byte{},0}
pow:= NewProofOfWork(block)
nonce ,hash :=pow.Run()
block.Hash =hash[:]
block.Nonce =nonce
return block
}
由于改造后新区块的定义添加了Nonce ,所以我们要对Nonce也进行赋值,并且我们要通过poW算法来重新生成区块。
验证区块
本文在前面说过,区块的数据有任意改动,哪怕是一个字节的改动,Hash值都会随着变化。因此在对于新生成的区块,我们非常有必要对它重新计算hash,验证是否合法。但由于本文代码仅是区块链简单演示,区块验证并非必要。
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
}
运行
func main() {
bc := NewBlockchain()
fmt.Printf("blockChain : %v\n", bc)
bc.AddBlock("Aimi send 100 BTC to Bob")
bc.AddBlock("Aimi send 100 BTC to Jay")
bc.AddBlock("Aimi send 100 BTC to Clown")
length := len(bc.blocks)
fmt.Printf("length of blocks : %d\n", length)
for i := 0; i < length; i++ {
pow :=NewProofOfWork(bc.blocks[i])
if pow.Validate() {
fmt.Println("—————————————————————————————————————————————————————")
fmt.Printf(" Block: %d\n",bc.blocks[i].Index)
fmt.Printf("Data: %s\n",bc.blocks[i].Data)
fmt.Printf("TimeStamp: %d\n",bc.blocks[i].TimeStamp)
fmt.Printf("Hash: %x\n",bc.blocks[i].Hash)
fmt.Printf("PrevHash: %x\n",bc.blocks[i].PrevBlockHash)
fmt.Printf("Nonce: %d\n",bc.blocks[i].Nonce)
}else {
fmt.Println("illegal block")
}
}
}
运行结果
Mining the block containing "first block"
000004ac82e7e15669e92dc048c77a83275730ad609432b20fc978ebefccb280
碰撞次数: 1838903
blockChain : &{[0xc000088120]}
Mining the block containing "Aimi send 100 BTC to Bob"
00000f043bb864157d44331de46b3e7ef26834e106f34532e40c83ad5d3695db
碰撞次数: 470193
Mining the block containing "Aimi send 100 BTC to Jay"
00000e5f5eebe8a8d5b0eb63fc065bf9af7f0d77e9b8a95201946a3dd4fab074
碰撞次数: 653569
Mining the block containing "Aimi send 100 BTC to Clown"
00000005c53d949c14acc4fb8756a8b2db986c4b5f43d7c0b75d93b0d5c06559
碰撞次数: 36035
length of blocks : 4
—————————————————————————————————————————————————————
Block: 0
Data: first block
TimeStamp: 1538999762
Hash: 000004ac82e7e15669e92dc048c77a83275730ad609432b20fc978ebefccb280
PrevHash:
Nonce: 1838903
—————————————————————————————————————————————————————
Block: 1
Data: Aimi send 100 BTC to Bob
TimeStamp: 1538999769
Hash: 00000f043bb864157d44331de46b3e7ef26834e106f34532e40c83ad5d3695db
PrevHash: 000004ac82e7e15669e92dc048c77a83275730ad609432b20fc978ebefccb280
Nonce: 470193
—————————————————————————————————————————————————————
Block: 2
Data: Aimi send 100 BTC to Jay
TimeStamp: 1538999771
Hash: 00000e5f5eebe8a8d5b0eb63fc065bf9af7f0d77e9b8a95201946a3dd4fab074
PrevHash: 00000f043bb864157d44331de46b3e7ef26834e106f34532e40c83ad5d3695db
Nonce: 653569
—————————————————————————————————————————————————————
Block: 3
Data: Aimi send 100 BTC to Clown
TimeStamp: 1538999774
Hash: 00000005c53d949c14acc4fb8756a8b2db986c4b5f43d7c0b75d93b0d5c06559
PrevHash: 00000e5f5eebe8a8d5b0eb63fc065bf9af7f0d77e9b8a95201946a3dd4fab074
Nonce: 36035
总结
相比上一章,本文增加了poW挖矿功能,我们离区块链又近了一步。工作量证明说白了,就是在一定的时间内,算出特定的值,使hash结果符合特定的要求。下一章我们将实现区块链的本地化存储功能。