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

好的,我们已经完成了理论,让我们编写代码!首先,让我们来定义挖掘的难度:

const targetBits = 24

在比特币中,当一个块被挖出来以后,“target bits” 代表了区块头里存储的难度,也就是开头有多少个 0。目前我们不会实现目标调整算法,因此我们可以将难度定义为全局常量。

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 结构,里面存储了指向一个块(block)和一个目标(target)的指针。这里的 “目标” ,也就是前一节中所描述的必要条件。这里使用了一个 大整数 ,我们会将哈希与目标进行比较:先把哈希转换成一个大整数,然后检测它是否小于目标。

NewProofOfWork 函数中,我们将 big.Int 初始化为 1,然后左移 256 - targetBits 位。256 是一个 SHA-256 哈希的位数,我们将要使用的是 SHA-256 哈希算法。target(目标) 的 16 进制形式为:

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
}

这个部分很简单:我们只是将 target ,nonce 与 Block 进行合并。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实现的难度太低而不能使计数器溢出,但为了以防万一,进行此检查仍然更好。

在这个循环中,我们做的事情有:

  1. 准备数据
  2. 用 SHA-256 对数据进行哈希
  3. 将哈希转换成一个大整数
  4. 将这个大整数与目标进行比较

跟之前所讲的一样简单。现在我们可以移除 BlockSetHash 方法,然后修改 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 的一个属性。这是十分有必要的,因为待会儿我们对这个工作量进行验证时会用到 nonceBlock 结构现在看起来像是这样:

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()
	}
}
复制代码

Output:

...

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

我们的区块链更接近其实际架构:现在添加区块需要努力工作,因此可以进行挖掘。但它仍然缺乏一些关键特征:区块链数据库不是持久的,没有钱包,地址,交易,也没有共识机制。所有这些我们将在未来的文章中实现,现在,快乐的采矿吧!


英文原文:https://jeiwan.cc/posts/building-blockchain-in-go-part-2/