Go语言实现区块链与加密货币-Part1(基本原型、工作量证明、持久化)

2023-11-18

区块链(Blockchain)是21世纪最具革命性的技术之一,它仍然处于不断成长的阶段,而且还有很多潜力尚未显现。作为比特币的底层技术,它本质上只是一个分布式数据库。不过使它独一无二的是,区块链是一个公开的而不是私人的数据库,每个使用它的人都有一个完整或者部分的副本。只有经过其他“数据库管理员”的同意,才能向其中添加新的记录。此外,也正是由于区块链,才使得加密货币和智能合约成为现实。

本文将实现一个简化版的区块链,并基于此来构建一个简化版的加密货币。

准备工作:
1.Go语言(可选)教程以及环境安装:http://www.runoob.com/go/go-environment.html
2.安装数据库依赖包:$ go get -u github.com/boltdb/bolt
参考:
https://jeiwan.cc/
https://liuchengxu.gitbook.io/blockchain/
目的:
1.认识区块链,了解其基本数据结构;
2.理解共识算法PoW的基本原理和作用;
3.实现持久化和命令行接口。

1、基本数据结构

首先从 “区块” 谈起。在区块链中,真正存储有效信息的是区块(block)。而在比特币中,真正有价值的信息就是交易(transaction)。实际上,交易信息是所有加密货币的价值所在。除此以外,区块还包含了一些技术实现的相关信息,比如版本,当前时间戳和前一个区块的哈希。

不过,我们要实现的是一个简化版的区块链,而不是一个像比特币技术规范所描述那样成熟完备的区块链。所以在我们目前的实现中,区块仅包含了部分关键信息,它的数据结构如下:

//区块的数据结构
//Block 由区块头和交易信息两部分组成


type Block struct {
	Timestamp     int64   //当前时间戳
	Data          []byte  //区块实际存储的信息,也就是比特币中的交易信息
	PrevBlockHash []byte  //前一个块的哈希
	Hash          []byte  //当前块的哈希
}

在我们的简化版区块中,还有一个 Hash 字段,那么,要如何计算哈希呢?哈希计算,是区块链一个非常重要的部分。正是由于它,才保证了区块链的安全。计算一个哈希,是在计算上非常困难的一个操作。即使在高速电脑上,也要耗费很多时间 (这就是为什么人们会购买 GPU,FPGA,ASIC 来挖比特币) 。这是一个架构上有意为之的设计,它故意使得加入新的区块十分困难,继而保证区块一旦被加入以后,就很难再进行修改。在接下来的内容中,我们将会讨论和实现这个机制。

目前,我们仅取了 Block 结构的部分字段(Timestamp, Data 和 PrevBlockHash),并将它们相互拼接起来,然后在拼接后的结果上计算一个 SHA-256,然后就得到了哈希。把这个功能用以下的SetHash函数来实现。

//设置当前块哈希
// Hash=sha256(PrevBlockHash+Data+Timestamp)
func (b *Block) SetHash() {
	timestamp := []byte(strconv.FormatInt(b.Timestamp, 10))
	headers := bytes.Join([][]byte{b.PrevBlockHash, b.Data, timestamp}, []byte{})
	hash := sha256.Sum256(headers)

	b.Hash = hash[:]
}

接下来,按照 Golang 的惯例,我们会实现一个用于简化创建区块的函数 NewBlock:

//用于生成新块
//当前块的哈希会基于传入的参数Data 和PrevBlockHash计算得到
func NewBlock(data string, prevBlockHash []byte) *Block {
	block := &Block{
		Timestamp:		time.Now().Unix(), 
		PrevBlockHash:	prevBlockHash, 
		Hash:			[]byte{},
		Data:			[]byte(data) }
	
	block.SetHash()
	return block
}

有了区块,下面让我们来实现区块。本质上,区块链就是一个有着特定结构的数据库,是一个有序,每一个块都连接到前一个块的链表。也就是说,区块按照插入的顺序进行存储,每个块都与前一个块相连。这样的结构,能够让我们快速地获取链上的最新块,并且高效地通过哈希来检索一个块。
在 Golang 中,可以通过一个 array 和 map 来实现这个结构:array 存储有序的哈希(Golang 中 array 是有序的),map 存储 hash -> block 对(Golang 中, map 是无序的)。 但是在基本的原型阶段,我们只用到了 array,因为现在还不需要通过哈希来获取块。

//第一个区块链————这是一个Block 指针数组
type Blockchain struct {
	blocks []*Block
}

现在,让我们能够给它添加一个区块:

//添加区块
//data就是交易
func (bc *Blockchain) AddBlock(data string) {
	prevBlock := bc.blocks[len(bc.blocks)-1]
	newBlock := NewBlock(data, prevBlock.Hash)
	bc.blocks = append(bc.blocks, newBlock)
}

结束!不过,就这样就完成了吗?
为了加入一个新的块,我们必须要有一个已有的块,但是,初始状态下,我们的链是空的,一个块都没有!所以,在任何一个区块链中,都必须至少有一个块。这个块,也就是链中的第一个块,通常叫做创世块(genesis block)。(话说中本聪还在创世块中留下了经典名言) 让我们实现一个方法来创建创世块:

//创建创世区块
func NewGenesisBlock() *Block {
	return NewBlock("Genesis Block", []byte{})
}

现在,我们可以实现一个函数来创建一条有创世块的

//创建一个有创世块的区块链
func NewBlockchain() *Blockchain {
	return &Blockchain{[]*Block{NewGenesisBlock()}}
}

检查一下我们的区块链是否如期工作:

func main() {
    bc := NewBlockchain()
​
    bc.AddBlock("Send 1 BTC to Ivan")
    bc.AddBlock("Send 2 more BTC to Ivan")for _, block := range bc.blocks {
        fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
        fmt.Printf("Data: %s\n", block.Data)
        fmt.Printf("Hash: %x\n", block.Hash)
        fmt.Println()
    }
}

以上涉及到的Go语言包:

package main

import (
"bytes"
"crypto/sha256"
"strconv"
"time"
"fmt"
)

在命令行中运行结果如下:
在这里插入图片描述
我们创建了一个非常简单的区块链原型:它仅仅是一个数组构成的一系列区块,每个块都与前一个块相关联。真实的区块链要比这复杂得多。在我们的区块链中,加入新的块非常简单,也很快,但是在真实的区块链中,加入新的块需要很多工作:你必须要经过十分繁重的计算(这个机制叫做工作量证明),来获得添加一个新块的权力。并且,区块链是一个分布式数据库,并且没有单一决策者。因此,要加入一个新块,必须要被网络的其他参与者确认和同意(这个机制叫做共识(consensus))。还有一点,我们的区块链还没有任何的交易!

2、工作量证明(Proof-of-Work)

在上一节,我们构造了一个非常简单的数据结构 – 区块,它也是整个区块链数据库的核心。目前所完成的区块链原型,已经可以通过链式关系把区块相互关联起来:每个块都与前一个块相关联。
但是,当前实现的区块链有一个巨大的缺陷:向链中加入区块太容易,也太廉价了。而区块链和比特币的其中一个核心就是,要想加入新的区块,必须先完成一些非常困难的工作。在本文,我们将会弥补这个缺陷。
区块链的一个关键点就是,一个人必须经过一系列困难的工作,才能将数据放入到区块链中。正是由于这种困难的工作,才保证了区块链的安全和一致。此外,完成这个工作的人,也会获得相应奖励(这也就是通过挖矿获得币)。
这个机制与生活现象非常类似:一个人必须通过努力工作,才能够获得回报或者奖励,用以支撑他们的生活。在区块链中,是通过网络中的参与者(矿工)不断的工作来支撑起了整个网络。矿工不断地向区块链中加入新块,然后获得相应的奖励。在这种机制的作用下,新生成的区块能够被安全地加入到区块链中,它维护了整个区块链数据库的稳定性。值得注意的是,完成了这个工作的人必须要证明这一点,即他必须要证明他的确完成了这些工作。
整个 “努力工作并进行证明” 的机制,就叫做工作量证明(proof-of-work)。要想完成工作非常地不容易,因为这需要大量的计算能力:即便是高性能计算机,也无法在短时间内快速完成。另外,这个工作的困难度会随着时间不断增长,以保持每 10 分钟出 1 个新块的速度。在比特币中,这个工作就是找到一个块的哈希,同时这个哈希满足了一些必要条件。这个哈希,也就充当了证明的角色。因此,寻求证明(寻找有效哈希),就是矿工实际要做的事情。

2.1 哈希计算

获得指定数据的一个哈希值的过程,就叫做哈希计算。一个哈希,就是对所计算数据的一个唯一表示。对于一个哈希函数,输入任意大小的数据,它会输出一个固定大小的哈希值。下面是哈希的几个关键特性:
1.无法从一个哈希值恢复原始数据。也就是说,哈希并不是加密。
2.对于特定的数据,只能有一个哈希,并且这个哈希是唯一的。
3.即使是仅仅改变输入数据中的一个字节,也会导致输出一个完全不同的哈希。
在这里插入图片描述
在区块链中,哈希被用于保证一个块的一致性。哈希算法的输入数据包含了前一个块的哈希,因此使得不太可能(或者,至少很困难)去修改链中的一个块:因为如果一个人想要修改前面一个块的哈希,那么他必须要重新计算这个块以及后面所有块的哈希。

2.2 Hashcash

比特币使用 Hashcash ,一个最初用来防止垃圾邮件的工作量证明算法。它可以被分解为以下步骤:

1.取一些公开的数据(比如,如果是 email 的话,它可以是接收者的邮件地址;在比特币中,它是区块头)
2.给这个公开数据添加一个计数器。计数器默认从 0 开始
3.将 data(数据) 和 counter(计数器) 组合到一起,获得一个哈希
4.检查哈希是否符合一定的条件:
1.如果符合条件,结束
2.如果不符合,增加计数器,重复步骤 3-4
在这里插入图片描述
ca07ca 是计数器的 16 进制值,十进制的话是 13240266.

2.3 实现

与该部分相关的包:

ackage main

import (
"log"
"bytes"
"encoding/binary"
"crypto/sha256"
"strconv"
"time"
"fmt"
"math"
"math/big"
)

首先,定义挖矿的难度值.

//定义挖矿的难度值 ,以下表示哈希的前24位必须是0
const targetBits=24


在比特币中,当一个块被挖出来以后,“target bits” 代表了区块头里存储的难度,也就是开头有多少个 0。这里的 24 指的是算出来的哈希前 24 位必须是 0,如果用 16 进制表示,就是前 6 位必须是 0,这一点从最后的输出可以看出来。目前我们并不会实现一个动态调整目标的算法,所以将难度定义为一个全局的常量即可。
24 其实是一个可以任意取的数字,其目的只是为了有一个目标(target)而已,这个目标占据不到 256 位的内存空间。同时,我们想要有足够的差异性,但是又不至于大的过分,因为差异性越大,就越难找到一个合适的哈希。

//每个块的工作量都必须要证明,所以有个指向Block的指针
//target是目标,我们最终要找的哈希必须要小于目标
type ProofOfWork struct {
	block  *Block
	target *big.Int
}


//target等于1左移256-targetBits 位?
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 进制形式为:
在这里插入图片描述
它在内存上占据了 29 个字节。下面是与前面例子哈希的形式化比较:
在这里插入图片描述
第一个哈希(基于 “I like donuts” 计算)比目标要大,因此它并不是一个有效的工作量证明。第二个哈希(基于 “I like donutsca07ca” 计算)比目标要小,所以是一个有效的证明。

你可以把目标想象为一个范围的上界:如果一个数(由哈希转换而来)比上界要小,那么是有效的,反之无效。因为要求比上界要小,所以会导致有效数字并不会很多。因此,也就需要通过一些困难的工作(一系列反复地计算),才能找到一个有效的数字。

现在,我们需要有数据来进行哈希,准备数据:

//工作量证明需要用到的数据有:PrevBlockHash, Data, Timestamp, targetBits, nonce(计数器,密码学术语)
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 所提到的计数器,它是一个密码学术语。

其中调用的IntToHex函数如下:

//将一个 int64 转化为一个字节数组(byte array)
func IntToHex(num int64) []byte {
	buff:=new(bytes.Buffer)
	err:=binary.Write(buff, binary.BigEndian, num)
	if err !=nil{
		log.Panic(err)
		}

	return buff.Bytes()
	
}

好,到这里,所有的准备工作就完成了,下面来实现 PoW 算法的核心:

var (maxNonce = math.MaxInt64)  //对循环进行限制


//Pow算法的核心就是寻找有效哈希
func (pow *ProofOfWork) Run() (int, []byte) {
	var hashInt big.Int //hashInt是hash的整形表示
	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)        //对数据进行哈希计算
		hashInt.SetBytes(hash[:])         //将将哈希转换成一个大整数

		if hashInt.Cmp(pow.target) == -1 {   //将大整数与目标进行比较
		    fmt.Printf("\r%x", hash)
			break
		} else {
			nonce++
		}
	}
	fmt.Print("\n\n")

	return nonce, hash[:]
}

首先我们对变量进行初始化:
HashInt 是 hash 的整形表示;
nonce 是计数器。

然后开始一个 “无限” 循环:maxNonce 对这个循环进行了限制, 它等于 math.MaxInt64,这是为了避免 nonce 可能出现的溢出。尽管我们 PoW 的难度很小,以至于计数器其实不太可能会溢出,但最好还是以防万一检查一下。

在这个循环中,我们做的事情有:
1.准备数据
2.用 SHA-256 对数据进行哈希
3.将哈希转换成一个大整数
4.将这个大整数与目标进行比较

现在我们可以移除 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   //在对工作量证明进行验证时用到
}

还剩下一件事情需要做,对工作量证明进行验证:

//验证工作量,只要哈希小于目标就是有效工作量
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
}

这里,就是我们就用到了上面保存的 nonce。
好了!现在让我们来运行一下是否正常工作:

//测试
func main() {
	bc := NewBlockchain()

	bc.AddBlock("Send 1 BTC to Ivan")
	bc.AddBlock("Send 2 more BTC to Ivan")

	for _, block := range bc.blocks {
		fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
		fmt.Printf("Data: %s\n", block.Data)
		fmt.Printf("Hash: %x\n", block.Hash)
		fmt.Println()
		pow := NewProofOfWork(block)
		fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))  //FormatBool 将布尔值转换为字符串 "true" 或 "false"
		fmt.Println()
	}
}

在这里插入图片描述
成功了!你可以看到每个哈希都是 3 个字节的 0 开始,并且获得这些哈希需要花费一些时间,这次我们产生三个块花费了一分多钟,比没有工作量证明之前慢了很多(也就是成本高了很多)。

我们离真正的区块链又进了一步:现在需要经过一些困难的工作才能加入新的块,因此挖矿就有可能了。但是,它仍然缺少一些至关重要的特性:区块链数据库并不是持久化的,没有钱包,地址,交易,也没有共识机制。不过,所有的这些,我们都会在接下来的文章中实现,现在,愉快地挖矿吧!

3、 持久化和命令行接口

到目前为止,我们已经构建了一个有工作量证明机制的区块链。有了工作量证明,挖矿也就有了着落。虽然目前距离一个有着完整功能的区块链越来越近了,但是它仍然缺少了一些重要的特性。在今天的内容中,我们会将区块链持久化到一个数据库中,然后会提供一个简单的命令行接口,用来完成一些与区块链的交互操作。本质上,区块链是一个分布式数据库,不过,我们暂时先忽略 “分布式” 这个部分,仅专注于 “存储” 这一点。

目前,我们的区块链实现里面并没有用到数据库,而是在每次运行程序时,简单地将区块链存储在内存中。那么一旦程序退出,所有的内容就都消失了。我们没有办法再次使用这条链,也没有办法与其他人共享,所以我们需要把它存储到磁盘上。
那么,我们要用哪个数据库呢?实际上,任何一个数据库都可以。在 比特币原始论文 中,并没有提到要使用哪一个具体的数据库,它完全取决于开发者如何选择。 Bitcoin Core ,最初由中本聪发布,现在是比特币的一个参考实现,它使用的是 LevelDB。而我们将要使用的是…

3.1 BoltDB

因为它:
1.非常简洁
2.用 Go 实现
3.不需要运行一个服务器
4.能够允许我们构造想要的数据结构

本部分涉及到的包:

"github.com/boltdb/bolt"

并且使用前要安装对应依赖:
$ go get -u github.com/boltdb/bolt
Bolt 使用键值存储,这意味着它没有像 SQL RDBMS (MySQL,PostgreSQL 等等)的表,没有行和列。相反,数据被存储为键值对(key-value pair,就像 Golang 的 map)。键值对被存储在 bucket 中,这是为了将相似的键值对进行分组(类似 RDBMS 中的表格)。因此,为了获取一个值,你需要知道一个 bucket 和一个键(key)。

需要注意的一个事情是,Bolt 数据库没有数据类型:键和值都是字节数组(byte array)。鉴于需要在里面存储 Go 的结构(准确来说,也就是存储Block(块)),我们需要对它们进行序列化,也就说,实现一个从 Go struct 转换到一个 byte array 的机制,同时还可以从一个 byte array 再转换回 Go struct。虽然我们将会使用 encoding/gob 来完成这一目标,但实际上也可以选择使用 JSON, XML, Protocol Buffers 等等。之所以选择使用 encoding/gob, 是因为它很简单,而且是 Go 标准库的一部分。
虽然 BoltDB 的作者出于个人原因已经不在对其维护(见README), 不过关系不大,它已经足够稳定了,况且也有活跃的 fork:coreos/bblot

3.2 数据库结构

在开始实现持久化的逻辑之前,我们首先需要决定到底要如何在数据库中进行存储。为此,我们可以参考 Bitcoin Core 的做法:
简单来说,Bitcoin Core 使用两个 “bucket” 来存储数据:
1.其中一个 bucket 是 blocks,它存储了描述一条链中所有块的元数据
2.另一个 bucket 是 chainstate,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据

此外,出于性能的考虑,Bitcoin Core 将每个区块(block)存储为磁盘上的不同文件。如此一来,就不需要仅仅为了读取一个单一的块而将所有(或者部分)的块都加载到内存中。但是,为了简单起见,我们并不会实现这一点。详情可见 (https://en.bitcoin.it/wiki/Bitcoin_Core_0.11_(ch_2):_Data_Storage)。

因为目前还没有交易,所以我们只需要 blocks bucket。另外,正如上面提到的,我们会将整个数据库存储为单个文件,而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有:
1.32 字节的 block-hash -> block 结构
2.l -> 链中最后一个块的 hash

这就是实现持久化机制所有需要了解的内容了。

3.3 序列化

本部分涉及到的包:

	"bytes"
	"encoding/gob"

上面提到,在 BoltDB 中,值只能是 []byte 类型,但是我们想要存储 Block 结构。所以,我们需要使用 encoding/gob 来对这些结构进行序列化。
让我们来实现 Block 的 Serialize 方法:

// 将block序列化为一个字节数组,这是一个方法
func (b *Block) Serialize() []byte {
	var result bytes.Buffer                 //Buffer用来存储序列化之后的数据
	// result:=new(bytes.Buffer)//分配内存,这是另一种写法,若用这种写法,则创建编码器时应该传入对象而不是地址
	
	encoder := gob.NewEncoder&result) 		//创建基于buf内存的编码器

	err := encoder.Encode(b)				//使用编码器对block结构体进行编码
	if err != nil {
		log.Panic(err)
	}

	return result.Bytes()					//结果作为一个字节数组返回

接下来,我们需要一个解序列化的函数,它会接受一个字节数组作为输入,并返回一个 Block指针.

// // 将字节数组反序列化为一个Block,这是一个单独的函数
func DeserializeBlock(d []byte) *Block {
	var block Block
	derusult:=bytes.NewBuffer(d)  			//使用result里面的数据创建初始化Buffer
	decoder:=gob.NewDecoder(derusult)		//	创建解码器
	//decoder := gob.NewDecoder(bytes.NewReader(d))  //这是另一种写法,创建解码器,传入的是d字节数组的Reader
	err := decoder.Decode(&block)      //对于d内容解码,并将解码后的内容写入变量block的内存中
	if err != nil {
		log.Panic(err)
	}

	return &block
}

这就是序列化部分的内容了。
关于gob本人总结了一些需要注意的地方:

1,结构体中的属性必须大写开头。不然无法序列化

2,序列化的struct与反序列化的struct结构可以不一样。只会匹配属性相同的数据。

3.4 持久化

让我们从 NewBlockchain 函数开始。在之前的实现中,NewBlockchain 会创建一个新的 Blockchain 实例,并向其中加入创世块。而现在,我们希望它做的事情有:
1.打开一个数据库文件
2.检查文件里面是否已经存储了一个区块链
3.如果已经存储了一个区块链:
——1.创建一个新的 Blockchain 实例
——2.设置 Blockchain 实例的 tip 为数据库中存储的最后一个块的哈希
4.如果没有区块链:
——1.创建创世块
——2.存储到数据库
——3.将创世块哈希保存为最后一个块的哈希
——4.创建一个新的 Blockchain 实例,初始时 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)
代码如下:

// N创建一个带有创世区块的区块链
func NewBlockchain() *Blockchain {
	var tip []byte

	db, err := bolt.Open(dbFile, 0600, nil)	//这是打开一个BoltDB文件的标准做法。注意,即便不存在这样的文件,它也不会返回错误
	//在BoltDB中,数据库操作通过一个事务(transaction)进行操作
	//这里打开的是一个读写事务(db.Update(...)),因为我们可能会向数据库中添加创世块
	if err != nil {
		log.Panic(err)
	}

	err = db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket)) //函数的核心,先获取存储区块的bucket,名为“blocks”

		if b == nil {			//如果数据库中不存在区块链(bucket为空),那么就创建一个,否则直接读取最后一个块的哈希
			fmt.Println("No existing blockchain found. Creating a new one...")
			genesis := NewGenesisBlock()

			b, err := tx.CreateBucket([]byte(blocksBucket))    //创建一个名为“blocks”的Bucket
			if err != nil {
				log.Panic(err)
			}

			err = b.Put(genesis.Hash, genesis.Serialize())  //将创世区块序列化后,与该块的哈希(作为键值)一起存入Bucket
			if err != nil {
				log.Panic(err)
			}

			err = b.Put([]byte("l"), genesis.Hash)  //用“1”作为创世哈希的键值,因为此时创世块作为最后一个块存在
			if err != nil {
				log.Panic(err)
			}
			tip = genesis.Hash  //指向创世区块
		} else {
			tip = b.Get([]byte("l")) //此时“1”是最后一个块的键值
		}

		return nil
	})

	if err != nil {
		log.Panic(err)
	}

	bc := Blockchain{tip, db}  //这是创建Blockchain的一个新方式

	return &bc
}

这次,我们不在里面存储所有的区块了,而是仅存储区块链的 tip。另外,我们存储了一个数据库连接。因为我们想要一旦打开它的话,就让它一直运行,直到程序运行结束。因此,Blockchain 的结构现在是这样:

// 区块链的结构体
//tip这个词本身有事物尖端或尾部的意思,这里指的是存储最后一个块的哈希
//db 存储数据库链接
type Blockchain struct {
	tip []byte
	db  *bolt.DB
}

接下来我们想要更新的是 AddBlock 方法:现在向链中加入区块,就不是像之前向一个数组中加入一个元素那么简单了。从现在开始,我们会将区块存储在数据库里面:

// 加入区块时,需要将区块持久化到数据库中
func (bc *Blockchain) AddBlock(data string) {
	var lastHash []byte

	err := bc.db.View(func(tx *bolt.Tx) error {  //这是BoltDB事务的另一个类型(只读)
		b := tx.Bucket([]byte(blocksBucket))
		lastHash = b.Get([]byte("l"))   //首先获取最后一个块的哈希用来生成新的哈希

		return nil
	})

	if err != nil {
		log.Panic(err)
	}

	newBlock := NewBlock(data, lastHash)

	err = bc.db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		err := b.Put(newBlock.Hash, newBlock.Serialize())
		if err != nil {
			log.Panic(err)
		}

		err = b.Put([]byte("l"), newBlock.Hash)//用“1”作为最后一个块的键值
		if err != nil {
			log.Panic(err)
		}

		bc.tip = newBlock.Hash  

		return nil
	})
}

3.5 检查区块链

现在,产生的所有块都会被保存到一个数据库里面,所以我们可以重新打开一个链,然后向里面加入新块。但是在实现这一点后,我们失去了之前一个非常好的特性:再也无法打印区块链的区块了,因为现在不是将区块存储在一个数组,而是放到了数据库里面。让我们来解决这个问题!
BoltDB 允许对一个 bucket 里面的所有 key 进行迭代,但是所有的 key 都以字节序进行存储,而且我们想要以区块能够进入区块链中的顺序进行打印。此外,因为我们不想将所有的块都加载到内存中(因为我们的区块链数据库可能很大!或者现在可以假装它可能很大),我们将会一个一个地读取它们。故而,我们需要一个区块链迭代器(BlockchainIterator):

//区块链迭代器的结构体
type BlockchainIterator struct {
	currentHash []byte
	db          *bolt.DB
}

每当要对链中的块进行迭代时,我们就会创建一个迭代器,里面存储了当前迭代的块哈希(currentHash)和数据库的连接(db)。通过 db,迭代器逻辑上被附属到一个区块链上(这里的区块链指的是存储了一个数据库连接的 Blockchain 实例),并且通过 Blockchain 方法进行创建:


// Blockchain中的迭代器方法 ...
func (bc *Blockchain) Iterator() *BlockchainIterator {
	bci := &BlockchainIterator{bc.tip, bc.db}

	return bci   //返回一个区块链迭代器的指针
}

注意,迭代器的初始状态为链中的 tip,因此区块将从尾到头(创世块为头),也就是从最新的到最旧的进行获取。实际上,选择一个 tip 就是意味着给一条链“投票”。一条链可能有多个分支,最长的那条链会被认为是主分支。在获得一个 tip (可以是链中的任意一个块)之后,我们就可以重新构造整条链,找到它的长度和需要构建它的工作。这同样也意味着,一个 tip 也就是区块链的一种标识符。
BlockchainIterator 只会做一件事情:返回链中的前一个块。

// 区块链迭代器的唯一功能:返回链中的前一个块
func (i *BlockchainIterator) Next() *Block {
	var block *Block

	err := i.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		encodedBlock := b.Get(i.currentHash)	//取出当前哈希这个键所对应的值,也就是当前块序列化后的字节码
		block = DeserializeBlock(encodedBlock) //解序列化,得到当前块的内容

		return nil
	})

	if err != nil {
		log.Panic(err)
	}

	i.currentHash = block.PrevBlockHash //把前一个块的哈希赋给迭代器中的“当前哈希”,也就是往上迭代

	return block
}

以上这就是数据库部分的内容。

3.6 CLI

本部分涉及到的包:

import (
	"flag"
	"fmt"
	"log"
	"os"
	"strconv"
)

到目前为止,我们的实现还没有提供一个与程序交互的接口:目前只是在 main 函数中简单执行了 NewBlockchain 和 bc.AddBlock 。是时候改变了!现在我们想要拥有这些命令,可以与命令行进行交互:
$ blockchain addblock “Pay 0.031337 for a coffee”
$ blockchain printchain
所有命令行相关的操作都会通过 CLI 结构进行处理:

//CLI负责处理命令行参数
type CLI struct {
	bc *Blockchain
}

它的 “入口” 是 Run 函数:

// Run负责解析命令行参数和处理命令
func (cli *CLI) Run() {
	cli.validateArgs()
	//使用标准库里面的flag包来解析命令行参数:
	//首先创建两个子命令:addBlock 和 printChain
	addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
	printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
	//然后给addblock 添加 -data标志,printchain 没有任何标志
	addBlockData := addBlockCmd.String("data", "", "Block data") //?自定义内容
	//然后,我们检查用户提供的命令,解析相关的 flag 子命令:
	switch os.Args[1] {
	case "addblock":
		err := addBlockCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	case "printchain":
		err := printChainCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	default:
		cli.printUsage()
		os.Exit(1)
	}
	//接着检查是哪个子命令并调用相关参数
	if addBlockCmd.Parsed() {
		if *addBlockData == "" {
			addBlockCmd.Usage()
			os.Exit(1)
		}
		cli.addBlock(*addBlockData)
	}

	if printChainCmd.Parsed() {
		cli.printChain()
	}
}

这部分内容跟之前的很像,唯一的区别是我们现在使用的是 BlockchainIterator 对区块链中的区块进行迭代。
其中几个方法的实现:

func (cli *CLI) printUsage() {
	fmt.Println("Usage:")
	fmt.Println("  addblock -data BLOCK_DATA - add a block to the blockchain")
	fmt.Println("  printchain - print all the blocks of the blockchain")
}

func (cli *CLI) validateArgs() {
	if len(os.Args) < 2 {
		cli.printUsage()
		os.Exit(1)
	}
}

func (cli *CLI) addBlock(data string) {
	cli.bc.AddBlock(data)
	fmt.Println("Success!")
}

func (cli *CLI) printChain() {
	bci := cli.bc.Iterator()

	for {
		block := bci.Next()

		fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
		fmt.Printf("Data: %s\n", block.Data)
		fmt.Printf("Hash: %x\n", block.Hash)
		pow := NewProofOfWork(block)
		fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
		fmt.Println()

		if len(block.PrevBlockHash) == 0 {
			break
		}
	}
}

记得不要忘了对 main 函数作出相应的修改:

func main() {
	bc := NewBlockchain()   
	defer bc.db.Close()

	cli := CLI{bc}
	cli.Run()
}

注意,无论提供什么命令行参数,都会创建一个新的链。

结束前再次提醒,记得安装依赖包!!!
$ go get -u github.com/boltdb/bolt

来看一下是不是如期工作:
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Go语言实现区块链与加密货币-Part1(基本原型、工作量证明、持久化) 的相关文章

  • 软件测试—第六章白盒测试基本路径测试法

    一 基本路径法 1 为什么使用基本路径法 一是降低了测试用例设计的难度 只要搞清了各种流程 就可以设计出高质量的测试用例来 而不用太多测试方面的经验 二是在测试时间较紧的情况下 可以有的放矢的选择测试用例 而不用完全根据经验来取舍 2 什么
  • 如何设置当我们点击鼠标右键时,可以有新建Typora的选项

    最近研究了一下注册表 感觉这个东西还是挺有意思的 今天周末放假 打算写一点日记 当然是不发表的那种 然后我果断打开了Typora 不过由于好久没用Typora了 我忘记把他放在哪里了 找了一段时间终于找到了 我突然想到 为什么不把Typor

随机推荐

  • 目标检测算法之YOLOV3

    本博客中YOLO系列均为个人理解笔记 欢迎评论指出理解有误或者要讨论的地方 YOLOV3模型相比于v2来说 实质性的改进并不大 更多的是一些技术的堆叠 其并不像yolov2对于v1一样 由巨大的改变和提升 其相对于yolov2 改变在于 1
  • Java并发(多线程和锁机制)

    part 01 Java线程 1 进程和线程的区别 进程是操作系统进行资源分配的最小单位 线程是操作系统进行任务分配的最小单位 一个进程可以有多个线程 2 Java开启线程的方式 继承Tread类 重写run方法 实现Runnable接口
  • 红日安全靶机实战(一)

    0x01 入侵web服务器 0x 1 1 信息收集 首先使用 nmap 来扫描ip 段存活的主机 nmap sn 192 168 127 0 24 这里网上有的用 netdiscover 来扫描 其实和 nmap sn 扫描原理是一样的 都
  • .secret勒索病毒数据恢复

    导言 在数字时代 随着科技的迅猛发展 我们的生活更加便捷 但也引发了一系列新的威胁 其中之一就是勒索软件 而 secret 勒索病毒 则是这个威胁中的一颗明亮而又毒辣的星 91数据恢复本文将带您深入了解 secre 勒索病毒的特点 并探讨如
  • Image Sensor的FSIN/VSYNC

    本文介绍Image Sensor的FSIN VSYNC 产品开发过程 比如3D成像 中 有时会遇到需要2个及以上的Image Sensor同步采集 因此 Image Sensor厂家对于他们的产品都提供了同步功能 也就是我们经常所见的FSI
  • 学习Vue基础的分享

    Vue是一个前端框架 所以就有自己特定的语法 所以在这里列出学到的基础语法作用 基础模板参考 div h1 testName a baidu a h1 div
  • c语言ofstream未定义标识符,关于c++:c中ifstream及ofstream超详细说明

    前文说过 ifstream是继承于istream ofstream是继承于ostream fstream是继承于iostream类 而他们应用的缓冲区类是filebuf 对于这些类之间的关系 有趣味能够去查看我之前的文章 c 规范输入输出流
  • httpclient错误

    Caused by org apache http ProtocolException Target host is not specified at org apache http impl conn DefaultRoutePlanne
  • 用python实现计算器

    上次我用我学习的python做一个简易的计算器 我对计算器进行了 更改优化 变成了一个真正的计算器 实现流程 1 计算机布局 2 计算机执行 首先导入模块 Tkinter 作为 Python GUI 开发工具之一 它具有 GUI 软件包的必
  • 技术大佬和普通程序员改bug的区别!

    阅读本文大概需要2min 文 强哥 未经授权禁止转载 在我这么多年的工作生涯里 难免遇到那些工作糊弄的开发同事 随意编程的实习生 不够细致的测试 缺乏专业度的产品 产品的体验 取决于多个环节的把控 但很多情况下 由于bug严重影响体验 或者
  • 【原创】遇到 ORACLE 错误 1017

    1 错误描述 expdp sys leixiao orcl1 schemas sys directory DATA PUMP DIR dumpfile expdp test1 dmp logfile expdp test1 log 以sys
  • 做影视剪辑短视频,新手小白一个月赚5000多,用点心你也可以

    大周有一个学员把短视频当自己兴趣爱好 空余时间做二次剪辑 谁想到一个月多赚了5000多 他在一家私企上班 工作还算稳定 每天朝九晚五的工作 他不想就这样日复一日平淡的过下去 找到了大周 开始了自己短视频之旅 他这个人比较腼腆 本人出镜拍视频
  • 解决Android通过chrome://inspect/调试WebView出现 HTTP/1.1 404 Not Found 的问题

    问题描述 无论是调试Web页面还是调试Hybrid混合应用 只要是调试Android的webview 都需要使用Chrome inspect进行调试 但是国内开发者会出现404 Not Found错误 原因解析 国内网络无法访问 https
  • Java中九大内置对象

    1 Request对象 该对象封装了用户提交的信息 通过调用该对象相应的方法可以获取封装的信息 即使用该对象可以获取用户提交的信息 当Request对象获取客户提交的汉字字符时 会出现乱码问题 必须进行特殊处理 首先 将获取的字符串用ISO
  • 全国计算机等级考试题库二级C操作题100套(第91套)

    第91套 函数fun的功能是 计算请在程序的下划线处填入正确的内容并把下划线删除 使程序得出正确的结果 注意 源程序存放在考生文件夹下的BLANK1 C中 不得增行或删行 也不得更改程序的结构 给定源程序 include
  • Spring Cloud 2.2.2 源码之三十九nacos配置动态刷新原理一

    Spring Cloud 2 2 2 源码之三十九nacos配置动态刷新原理一 RefreshScope注解类实例化基本流程 nacos如何通过RefreshScope注解进行属性刷新 RefreshEventListener的handle
  • Selenium安装及环境配置

    目录 一 Selenium 简介 1 组件 2 特点 二 安装Selenium 三 下载对应版本的Chromedriver 1 查看Chrome的版本号 2 下载驱动 chromedriver和配置 3 解压到本地 4 复制文件放入pyth
  • 一次吃透Qt中信号与槽(包含信号与槽的使用,自定义以及重构示例,建议收藏)

    1 Qt中信号和槽 信号与槽 信号与槽 Signal Slot 是 Qt 编程的基础 也是 Qt 的一大创新 因为有了信号与槽的编程机制 在 Qt 中处理界面各个组件的交互操作时变得更加直观和简单 它可以让应用程序编程人员把这些互不了解的对
  • 2023领导力测评启示录

    导读 在现今这个由数据驱动的世界里 人力资源专业人士也越来越注重在进行人才选拔和发展时 运用客观数据来辅助决策 然而 面对市场上种类繁多的测评选择 首要挑战就是要了解不同类型的领导力测评 通常 测评主要分为两类 这两类测评的区别在于收集的数
  • Go语言实现区块链与加密货币-Part1(基本原型、工作量证明、持久化)

    区块链 Blockchain 是21世纪最具革命性的技术之一 它仍然处于不断成长的阶段 而且还有很多潜力尚未显现 作为比特币的底层技术 它本质上只是一个分布式数据库 不过使它独一无二的是 区块链是一个公开的而不是私人的数据库 每个使用它的人