之前我们介绍过如何通过Go语言实现pow挖矿,今天我们继续来自己动手编写区块链。之前我们的区块链即使运行起来,停下来之后数据就会丢失,那么数据怎样做到持久化呢?这和我们之前学到的原理是一样的,在进程中的数据会随着进程的结束而消失,若想持久化,那必须把数据保存在磁盘,大家很容易就联想到数据库。
为了让我们的区块链数据能够持久化,我们也使用一款数据库,这款数据库不能像oracle和mysql那样麻烦,还需要安装、配置等等,我们只需要将数据保留在本地就可以了,因此我们使用一款小巧型的数据库-boltdb,初看这个名字,竟然是与短跑名将齐名!
先不着急把数据存放到数据库中,首先我们要解决一个小安全问题,因为数据一旦存储到数据库中,就代表着可以被其他任何第三方读取到,为了保护我们的数据,我们将数据做一些小小的处理,之前我们提到过的,做一下数据序列化。在这里我们使用golang中的gob库来进行序列化处理,当然序列化之后我们自己也要有解析的能力。
// Serialize serializes the block
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
if err != nil {
log.Panic(err)
}
return result.Bytes()
}
Serialize() 可以将一个区块的数据变成我们无法识别的符号。
// DeserializeBlock deserializes a block
func DeserializeBlock(d []byte) *Block {
var block Block
//构造解码器
decoder := gob.NewDecoder(bytes.NewReader(d))
//解码
err := decoder.Decode(&block)
if err != nil {
log.Panic(err)
}
return &block
}
DeserializeBlock则完全是逆向操作,帮助我们把数据还原为区块信息。
接下来我们考虑改造之前的区块链,将区块与数据库相结合。
// Blockchain keeps a sequence of Blocks
type Blockchain struct {
tip []byte //记录当前块的hash值
db *bolt.DB //保存区块完整数据
}
对于boltdb来说,简单的2个操作就够了,不过它内部有一个bucket的概念,我们首先决定把数据装到哪个bucket里,然后再考虑是存还是取的操作。
// NewBlockchain creates a new Blockchain with genesis Block
func NewBlockchain() *Blockchain {
var tip []byte
//打开数据库,相当于指定了一个数据库文件
db, err := bolt.Open(dbFile, 0600, nil)
if err != nil {
log.Panic(err)
}
//涉及到数据修改,要用到update方法
err = db.Update(func(tx *bolt.Tx) error {
//取指定的bucket
b := tx.Bucket([]byte(blocksBucket))
if b == nil {
fmt.Println("No existing blockchain found. Creating a new one...")
genesis := NewGenesisBlock()
//创世块需要创建新的bucket
b, err := tx.CreateBucket([]byte(blocksBucket))
if err != nil {
log.Panic(err)
}
//放创世块的hash与序列化数据
err = b.Put(genesis.Hash, genesis.Serialize())
if err != nil {
log.Panic(err)
}
//l代表之前块的hash值
err = b.Put([]byte("l"), genesis.Hash)
if err != nil {
log.Panic(err)
}
tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}
return nil
})
if err != nil {
log.Panic(err)
}
bc := Blockchain{tip, db}
return &bc
}
这样的函数可以帮助我们完成创世块的创建,同时区块链运行起来。接下来我们需要考虑增加一个区块:
// AddBlock saves provided data as a block in the blockchain
func (bc *Blockchain) AddBlock(data string) {
var lastHash []byte
//增加区块,意味着之前肯定有数据了,所以此时用view方法查看历史数据,以免误操作
err := bc.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
lastHash = b.Get([]byte("l"))
return nil
})
if err != nil {
log.Panic(err)
}
newBlock := NewBlock(data, lastHash)
//用pow构造一个新块后,我们将数据写入到数据库中,现在不再是依靠我们的数据结构来形成区块信息
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)
if err != nil {
log.Panic(err)
}
bc.tip = newBlock.Hash
return nil
})
}
当这些事情搞定后,我们还需要知道如何去遍历已经产生的区块,首先我们先定义一个迭代器结构:
// BlockchainIterator is used to iterate over blockchain blocks
type BlockchainIterator struct {
currentHash []byte
db *bolt.DB
}
// Iterator ...
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip, bc.db}
return bci
}
实现遍历就是通过当前块能找到下一块,这时候反序列化就有它的用武之地了!
// Next returns next block starting from the tip
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
}
为了让我们的区块链运行起来舒服一点点,我们来实现一个命令行的客户端。
type CLI struct {
bc *Blockchain
}
客户端的第一件事先提供一个帮助手册
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)
}
}
我们暂时将客户端只增加2个操作,添加区块与打印区块链
·添加区块
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
}
}
}
接下来,完成我们的客户端调用,run起来。
// Run parses command line arguments and processes commands
func (cli *CLI) Run() {
cli.validateArgs()
//参数设置
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")
//根据参数进行分支处理
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()
}
}
接下来的事情,在main函数里,把代码联系起来就可以了!
package main
func main() {
bc := NewBlockchain()
defer bc.db.Close()
cli := CLI{bc}
cli.Run()
}
大功告成,接下来我们需要来敲一些命令来看看效果了!
localhost:level3 yekai$ go run *.go
No existing blockchain found. Creating a new one...
Mining the block containing "Genesis Block"
00000005303bf13700fa0ec64153e8fb6c0a5978fbcc262c7c2e0b477510a5c1
Usage:
addblock -data BLOCK_DATA - add a block to the blockchain
printchain - print all the blocks of the blockchain
exit status 1
再加一个区块
localhost:level3 yekai$ go run *.go addblock -data "yekai's 100 btc to alice"
Mining the block containing "yekai's 100 btc to alice"
000000c28b0e005142d2494b4a5819d086c2180c76fbaa31d9fa23f98ede2ed3
Success!
打印一下看看
localhost:level3 yekai$ go run *.go printchain
Prev. hash: 00000005303bf13700fa0ec64153e8fb6c0a5978fbcc262c7c2e0b477510a5c1
Data: yekai's 100 btc to alice
Hash: 000000c28b0e005142d2494b4a5819d086c2180c76fbaa31d9fa23f98ede2ed3
PoW: true
Prev. hash:
Data: Genesis Block
Hash: 00000005303bf13700fa0ec64153e8fb6c0a5978fbcc262c7c2e0b477510a5c1
PoW: true
总结一下,我们使用了gob序列化技术将区块数据序列化,然后利用boltdb将hash与区块数据进行了一对一key-val存储,并用一个客户端通过命令行的形式进行展现。
网友评论