Introduction
在上一篇文章中,我们构建了一个非常简单的数据结构,这是区块链数据库的本质。我们可以通过它们之间的链状关系为它添加区块:每个区块都链接到前一个块。我们的区块链实现有一个重大缺陷:向链中添加区块很容易。区块链和比特币的核心之一是:添加新区块是一项艰苦的工作。今天我们要解决这个缺陷。
Proof-of-Work(工作量证明)
区块链的一个关键思想是,必须进行一些艰苦的工作才能将数据放入其中。正是这项艰苦的工作使区块链变得安全和一致。此外,这项艰苦的工作也得到了回报(这也就是通过挖矿获得币)。
这种机制与现实生活中的机制非常相似:人们必须努力工作,才能获得奖励并维持生命。在区块链中,网络的一些参与者(矿工)努力维持网络,向其添加新区块,并获得对其工作的奖励。由于他们的工作,区块以安全的方式被整合到区块链中,这保持了整个区块链数据库的稳定性。值得注意的是,完成工作的人必须证明这一点。
这整个“努力工作和证明”的机制被称为工作量证明。这很难,因为它需要大量的计算能力:即使是高性能的计算机也无法快速完成。另外,这个工作的困难度会随着时间不断增长,以保持每 10 分钟出 1 个新块的速度。在比特币中,这种工作的目标是找到一个块的哈希,满足一些要求。这个哈希,也就充当了证明的角色。因此,寻求证明(寻找有效哈希),就是矿工实际要做的事情。
最后要注意的一点是。工作量证明算法必须满足要求:完成工作很难,但验证证明很容易。证明通常会交给其他人,因此对他们而言,验证它不应该花费太多时间。
Hashing
在本段中,我们将讨论哈希。如果您熟悉这个概念,可以跳过这一部分。
哈希是获取指定数据的哈希的过程。哈希是计算数据的唯一表示。哈希函数是一种获取任意大小数据并生成固定大小哈希的函数。以下是哈希的一些主要功能:
1、无法从哈希中恢复原始数据。因此,哈希不是加密。
2、某些数据只能有一个哈希值,哈希值是唯一的。
3、改变输入数据中的一个字节将导致完全不同的哈希。
哈希函数广泛用于检查数据的一致性。除软件包外,某些软件提供商还会发布校验和。下载文件后,您可以将其提供给哈希函数,并将生成的哈希与软件开发人员提供的哈希进行比较。
在区块链中,哈希用于保证区块的一致性。哈希算法的输入数据包含前一个区块的哈希,因此无法(或者至少非常困难)修改链中的区块:必须重新计算其哈希和其后所有区块的哈希值。
Hashcash
比特币使用 Hashcash,一种最初开发用于防止电子邮件垃圾邮件的工作量证明算法。它可以分为以下几个步骤:
1、拿一些公开的数据(如果是电子邮件,它是接收者的电子邮件地址;对于比特币,它是块头)。
2、添加一个计数器。
3、计数器从0开始。将 data(数据) 和 counter(计数器) 组合到一起,获得一个哈希
4、检查哈希符合某些要求。如果确实如此,那就完成了。如果没有,请增加计数器并重复步骤3和4。
因此,这是一个暴力算法:你改变计数器,计算一个新的哈希,检查它,增加计数器,计算一个哈希等。这就是为什么它的计算成本很高。
现在让我们仔细看看哈希必须满足的要求。在最初的Hashcash实现中,它的要求是 “一个哈希的前 20 位必须是 0”。在比特币中,需求会不时调整,因为,尽管计算能力随着时间的推移而增加,并且越来越多的矿工加入网络,但必须保证每 10 分钟生成一个块。
为了演示这个算法,我从前面的例子中获取了数据(“我喜欢甜甜圈”)并找到了一个以3个零字节开头的哈希:
ca07ca是计数器的十六进制值,十进制系统中为13240266。
Implementation
好的,我们已经完成了理论,让我们编写代码!首先,让我们来定义挖掘的难度:
代码语言:javascript复制const targetBits = 24
在比特币中,当一个块被挖出来以后,“target bits” 代表了区块头里存储的难度,也就是开头有多少个 0。目前我们不会实现目标调整算法,因此我们可以将难度定义为全局常量。
24是一个任意数字,我们的目标是让一个目标在内存中占用少于256位。我们希望差异足够大,但不要太大,因为差异越大,找到合适的散列越困难。
代码语言:javascript复制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
)的指针。这里的 “目标” ,也就是前一节中所描述的必要条件。这里使用了一个 大整数 ,我们会将哈希与目标进行比较:先把哈希转换成一个大整数,然后检测它是否小于目标。
在 NewProofOfWork 函数中,我们将 big.Int 初始化为 1,然后左移 256 - targetBits
位。256 是一个 SHA-256 哈希的位数,我们将要使用的是 SHA-256 哈希算法。target(目标) 的 16 进制形式为:
0x10000000000000000000000000000000000000000000000000000000000
它在内存中占用29个字节。这里是与前面例子中的哈希的视觉比较:
代码语言:javascript复制0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3
0000010000000000000000000000000000000000000000000000000000000000
0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca
第一个哈希值(根据“我喜欢甜甜圈”计算)比目标大,因此它不是有效的工作证明。第二个哈希(计算在“我喜欢donutsca07ca”)小于目标,因此它是一个有效的证据。
您可以将目标视为范围的上边界:如果数字(哈希)低于边界,则它是有效的,反之亦然。降低边界将导致有效数字减少,因此找到有效数字所需的工作更加困难。
现在,我们需要数据进行哈希处理。我们准备吧:
代码语言:javascript复制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
}
这个部分很简单:我们只是将 target ,nonce 与 Block 进行合并。nonce这里是上面Hashcash描述的计数器,这是一个加密术语。
好的,所有准备工作都已完成,让我们实现PoW算法的核心:
代码语言:javascript复制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("nn")
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{}, 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
}
好的!让我们运行程序,看看一切是否正常:
代码语言:javascript复制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 1 BTC to Ivan
Hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804
Prev. hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804
Data: Send 2 more BTC to Ivan
Hash: 000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe
好极了!您可以看到每个哈希现在以三个零字节开始,并且需要一些时间来获取这些哈希值。还有一件事要做:对工作量证明进行验证。
代码语言:javascript复制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
}
这就是我们需要保存的随机数的地方。让我们再检查一切都没问题:
代码语言:javascript复制func main() {
...
for _, block := range bc.blocks {
...
pow := NewProofOfWork(block)
fmt.Printf("PoW: %sn", strconv.FormatBool(pow.Validate()))
fmt.Println()
}
}
Output:
代码语言:javascript复制...
Prev. hash:
Data: Genesis Block
Hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
PoW: true
Prev. hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
Data: Send 1 BTC to Ivan
Hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
PoW: true
Prev. hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
Data: Send 2 more BTC to Ivan
Hash: 000000e42afddf57a3daa11b43b2e0923f23e894f96d1f24bfd9b8d2d494c57a
PoW: true
Conclusion
我们的区块链更接近其实际架构:现在添加区块需要努力工作,因此可以进行挖掘。但它仍然缺乏一些关键特征:区块链数据库不是持久的,没有钱包,地址,交易,也没有共识机制。所有这些我们将在未来的文章中实现,现在,快乐的采矿吧!
(未经同意,请勿转载)