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