【区块链Go语言实现】Part 2:工作量证明机制POW
0x00 介绍
在上一篇文章中,我们建立了一个非常简单的数据结构,它是区块链数据库的本质。并且,我们实现了以类似链条关系的方式向其中添加区块的功能:每个区块都会链接到前一区块。然而,我们实现的区块链有一个严重的缺陷:向区块链中添加区块太过容易和廉价。区块链和比特币的基本原则之一是,要使添加新区块是一项繁重的工作。本文中我们将解决这个缺陷。
0x01 工作量证明(Proof-of-Work,POW)
区块链的一个关键思想是,为了向区块链中添加新的区块,添加者(或节点)必须执行一些繁重的工作,而正是这种繁重的工作确保了区块链的安全性和一致性。同时,系统也会为该工作支付一定的报酬(这也就是人们能够通过挖矿来获得比特币的原因)。
这种机制与现实生活中的情况非常类似:一个人必须努力工作来获得报酬,以维持他们的生活。在区块链中,网络中的一些参与者(矿工)通过工作来维持整个网络,添加新区块,并为他们的工作获得报酬。由于他们的工作,区块能够以一种安全的方式并入到区块链中,这保证了整个区块链数据库的稳定性。值得注意的是,完成这项工作的人必须证明这一点。
整个“努力工作并证明”的机制称为工作量证明机制。做到这一点很困难,因为它需要大量的计算能力,即使是高性能计算机也无法快速做到这一点。此外,这项工作的难度也会随着时间而增大,以此保持新区块的增长速率约为每小时6块。在比特币网络中,这项工作的目标是找到一个满足一定要求的区块散列值,正是这个散列值来作为工作量的证明。因此,找到一个工作量的证明才是实际的工作。
最后需要注意一点,工作量证明算法必须满足一个要求:虽然做这项工作很困难,但验证证明却很容易。通常情况下,工作量证明生成后,会将证明传递给其他人,所以对他们来说,验证它不应该花费太多时间。
0x02 计算哈希
在这一节中,我们将讨论哈希的计算。如果你熟悉这个概念,那么你可以跳过这一部分。
计算哈希是一个求取指定数据的哈希值的过程。哈希是待计算目标数据的一种独特表示。哈希函数是这样一个函数,它可以接受任意大小的数据,并能够生成一个固定大小的哈希值。下面列举了计算哈希的一些关键特性:
- 从一个哈希值无法恢复原始数据。因此,计算哈希并不是对数据加密。
- 固定数据只能有一个哈希值,且该哈希值是独一无二的。
- 即使改变输入数据的一个字节都将导致一个完全不同的哈希值。
哈希函数广泛用于检查数据的一致性。一些软件提供商在发布软件包时也会附带一个软件包的校验和,这样用户下载软件后,可以通过哈希函数计算软件包的哈希值,然后将该哈希值与软件提供商提供的哈希值进行比较,以此确定所下载软件的完整性和一致性。
在区块链中,哈希用来保证区块的一致性。哈希算法中输入的数据包含前一区块的哈希值,因此使得篡改区块链中的区块变得不可能(或者至少变得十分困难):篡改者必须重新计算该区块的哈希值,以及该区块之后所有区块的哈希值。
0x03 哈希现金(Hashcash)
比特币使用了哈希现金(Hashcash)算法,它是一种工作量证明算法,最初开发用来防止垃圾邮件。它可以分为以下步骤:
- 利用一些公开已知的数据(以邮件为例,可以是接收方的邮件地址;在比特币情况下,就是区块头)。
- 向其中添加一个计数器,计数器从0开始。
- 计算数据和计数器组合的哈希值。
- 校验上步中得到的哈希值以确保其满足特定的要求。
- 如果正常实现,那么到此结束。
- 否则,增加计数器的值,然后重复第3和第4步。
因此,这是一个暴力算法:你改变计数器的值,计算一个新的哈希,对其进行检查,增加计数器值,计算哈希,等等。这就是为什么需要大量计算的原因了。
现在,让我们深入了解一个哈希必须满足的要求。在最初的哈希现金机制实现中,这种要求类似于“哈希的前20位必须是零”。在比特币中,这种要求会随时调整,因为在设计上,必须保证每10分钟生成一块新的区块,尽管计算能力在随着时间的推移而提升,以及越来越多的矿工加入到网络中。
为了演示该算法,使用上一例子中的数据(“I like donuts”),我们得到一个前3字节为0的哈希:
ca07ca是计数器的十六进制值,对应的十进制值为13240266。
0x04 代码实现
到此,我们完成了理论部分,接下来我们开始编写代码!首先,我们定义挖矿的难度:
const targetBits =
在比特币中,“目标比特”是区块头,它存储了该区块被开采时的难度。现在,我们并不去实现一种目标调整算法,而是仅仅将难度定义为一个全局常量。
24可以是任意数,我们的目的是在内存中得到一个少于256位的目标值,并且我们希望差别足够重要,但不要太大,因为差别越大,那么找到一个合适的哈希将会越困难。
type ProofOfWork struct {
block *Block
target *big.Int
} func NewProofOfWork(b *Block) *ProofOfWork {
target := big.NewInt()
target.Lsh(target, uint(-targetBits)) pow := &ProofOfWork{b, target} return pow
}
此处创建了ProofOfWork结构体,它包含一个指向某个区块的指针,以及一个指向目标的指针。“目标”是前面段落中描述的要求的另一名字。考虑到我们将哈希与目标进行比较的方式,我们使用了一个大整数(big.Int):我们将哈希转换为一个大整数,并检查它的值是否小于目标。
在NewProofOfWork函数中,我们初始化一个big.Int的值为1, 并将其值左移256-targetBits位,其中256为一个SHA-256哈希的比特长度,我们将会使用SHA-256哈希算法。target(目标)的十六进制表示为:
0x10000000000000000000000000000000000000000000000000000000000
它在内存中占据了29个字节。下面是它与上一例子中哈希的视觉比较:
0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3 0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca
第一个哈希(对“I like donuts”计算得到)比目标大,因此它不是一个有效的工作量证明。第二个哈希(对“I like donutsca07ca”计算得到)则比目标小,因此这是一个有效的证明。
你可以想出一个目标作为一个范围的上限:如果一个数(一个哈希)低于边界,那么说明它是有效的,反之亦然。降低边界将会导致更少的有效数,因此,找到一个有效数将需要更困难的工作。
现在,我们需要待求取哈希的目标数据,下面我们准备该数据:
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
}
这段代码很直观:我们仅仅将区块字段、目标和nonce进行合并,此处的 nonce是前面描述的哈希现金算法中的计数器,这是一个密码学术语。
Ok,到此所有的准备工作都已完成。下面我们将实现PoW算法的核心:
func (pow *ProofOfWork) Run() (int, []byte) {
var hashInt big.Int
var hash []byte
nonce := 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) == - {
break
} else {
nonce++
}
}
fmt.Print("\n\n") return nonce, hash[:]
}
首先,我们初始化变量:hashInt是hash的整数表示;nonce是计数器。接下来,我们运行一个“无限”循环:该循环通过maxNonce值来限制,其值等于math.MaxInt64,这样做是为了避免可能的nonce溢出。虽然我们的PoW实现难度太低,以至于不会造成计数器的溢出,但最好还是对其进行验证,以防万一。
在循环中,我们做了以下事情:
- 准备数据。
- 使用SHA-256对其求取哈希。
- 将哈希转换成一个大整数。
- 将上步中的整数与目标进行比较。
正如我们前面解释的那样简单。现在我们可以删除Block的SetHash方法,并修改NewBlock函数:
func NewBlock(data string, prevBlockHash []byte) *Block {
block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, }
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
}
OK,现在我们运行程序来检查是否一切工作正常:
Mining the block containing "Genesis Block"
00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1 Mining the block containing "Send 1 BTC to Ivan"
00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804 Mining the block containing "Send 2 more BTC to Ivan"
000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe Prev. hash:
Data: Genesis Block
Hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1 Prev. hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1
Data: Send BTC to Ivan
Hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804 Prev. hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804
Data: Send more BTC to Ivan
Hash: 000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe
欧耶!你可以看到,现在每一个哈希都以三个零字节开头,而获取到这些哈希都需要花费一定的时间。
还有一件事需要做:实现验证工作量证明的功能。
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) == - return isValid
}
这就是我们需要用到保存的nonce的地方。
让我们再次检查以确保一切都OK:
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: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
PoW: true Prev. hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
Data: Send BTC to Ivan
Hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
PoW: true Prev. hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
Data: Send more BTC to Ivan
Hash: 000000e42afddf57a3daa11b43b2e0923f23e894f96d1f24bfd9b8d2d494c57a
PoW: true
0x05 结论
我们的区块链距离实际架构又近了一步:现在添加新的区块将需要繁重的工作,因此挖矿变得有可能。然而,它仍然缺少一些重要特点:我们的区块链数据库还没有持久化功能,且没有钱包、地址、交易,以及共识机制。我们将在后续文章中逐渐实现这些功能,现在就快乐地挖矿吧!
下一篇《【区块链Go语言实现】Part 3:持久化和CLI》中我们将介绍区块链中的持久化和CLI。
英文链接:https://jeiwan.cc/posts/building-blockchain-in-go-part-2/
【区块链Go语言实现】Part 2:工作量证明机制POW的更多相关文章
- 【区块链Go语言实现】Part 1:区块链基本原型
0x00 介绍 区块链(Blockchain)是21世纪最具革命性的技术之一,目前它仍处于逐渐成熟阶段,且其发展潜力尚未被完全意识到.从本质上讲,区块链只是一种记录的分布式数据库.但它之所以独特,是因 ...
- [GO]使用go语言实现比特币的工作量证明
之前的博文已经实现了区块连的基本的工作原理,但在比特币系统中有一个很重要的概念:工作量证明POW,在比特币系统中它的作用就是在十分钟左右的时间内只有一个有能够记帐并得到奖励 在之前的博文中,区块的哈希 ...
- [区块链\理解BTCD源码]GO语言实现一个区块链原型
摘要 本文构建了一个使用工作量证明机制(POW)的类BTC的区块链.将区块链持久化到一个Bolt数据库中,然后会提供一个简单的命令行接口,用来完成一些与区块链的交互操作.这篇文章目的是希望帮助大家理解 ...
- Rust 实现一个简单的区块链
一.背景 近期用 Rust 实现了 Jeiwan/blockchain_go,与原项目相比没有加入新的功能,只是换了一个编程语言实现了一遍,源码放在 Github 上. 开发这个项目,花费了好几个周末 ...
- python搭建区块链
#!/usr/bin/env python # encoding: utf-8 ''' 我们要创建一个 Blockchain 类 ,他的构造函数创建了一个初始化的空列表(要存储我们的区块链),并且另一 ...
- 区块链入门到实战(26)之以太坊(Ethereum) – 挖矿
以太坊(Ethereum)与其他公共区块链一样,使用工作量证明机制确保区块链网络正常运行. 矿工进行工作量证明计算,即挖矿,来选择区块,写入区块链,确认交易. 交易过程如下图所示: 从技术角度来看,以 ...
- 从Go语言编码角度解释实现简易区块链
区块链技术 人们可以用许多不同的方式解释区块链技术,其中通过加密货币来看区块链一直是主流.大多数人接触区块链技术都是从比特币谈起,但比特币仅仅是众多加密货币的一种. 到底什么是区块链技术? 从金融学相 ...
- cpp 区块链模拟示例(四) 区块链工作量证明
本文主要在之前的区块链原形上添加了工作量证明,并且为后继的交易功能做好准备. 上一个章节我们已经创建了区块链的基本原形,但是区块的哈希计算和加入太过于简单,如果按照这种速度添加区块那么区块链估计一个小 ...
- 区块链共识机制之工作量证明(POW)
像比特币.以太坊.NXT.Bitshares等这些区块链系统,其本质上是一种加密经济组织,它建立在点对点网络上,是去中心化.无管辖的,由密码学.经济学和社会共识来共同维护.这些加密网络因各种原因有着多 ...
随机推荐
- noip第24课作业
1. 马走日 [问题描述] 马在中国象棋以日子形规则移动.请编写一段程序给定n*m大小的棋盘,以及马的初始位置(x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点. ...
- hdu 4135 [a,b]中n互质数个数+容斥
http://acm.hdu.edu.cn/showproblem.php?pid=4135 给定一个数n,求某个区间[a,b]内有多少数与这个数互质. 对于一个给定的区间,我们如果能够求出这个区间内 ...
- Linux安全之SYN攻击原理及其应对措施
TCP自从1974年被发明出来之后,历经30多年发展,目前成为最重要的互联网基础协议,但TCP协议中也存在一些缺陷. SYN攻击就是利用TCP协议的缺陷,来导致系统服务停止正常的响应. SYN攻击原理 ...
- Linux下可视化空间分析工具ncdu
场景:磁盘空间占满后快速查找某个目录(子目录)占用空间大. ncdu /var (分析后按左右键查看即可)
- VS2015下安装Entity Framework Power Tools
Entity Framework Power Tools是一个由EntityFramework开发小组提供的工具,它可以从现有数据库生成Fluent款式的Code First代码. 怀着激动的心情安装 ...
- c++中的一些计算的问题
要实现小数的四舍五入, float a = 3.456; //保留到小数点后两位 float b =(int)((a * 100) + 0.5) / 100.0; 但是这样对负数不好使, 对负数的话, ...
- Django(ORM常用字段)
day68 参考:http://www.cnblogs.com/liwenzhou/p/8688919.html 1. Django ORM常用字段: 1. AutoField ...
- HDU - 2604 Queuing(递推式+矩阵快速幂)
Queuing Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Su ...
- python中的三次握手以及四次挥手
三次握手1.客户端对服务端说:我的序号是x,我要向你请求连接:(第一次握手,发送SYN包,然后进入SYN-SEND状态)2.服务端听到之后对客户端说:我的序号是y,期待你下一句序号是x+1的话(意思就 ...
- vue项目axios请求接口,后端代理请求接口404,问题出现在哪?
在vue项目中,列表数据需要用到qq音乐接口中的数据,但是直接请求不行,有host及referer限制,需要采用后端代理的方式.借助axios及node的express,在dev-server.js中 ...