Skip to content

持久化和命令行

现在有了 PoW ,可以挖矿了。现在需要将它存储起来,并且需要一个命令行的接口。

数据库

目前,区块链中没有用到数据库,每次运行程序会将数据库存储在内存中,程序一退出,所有的内容就都消失了。不能和别人共享了。
任何一个数据库都可以使用,在 BTC 的最初论文中,没有说具体要使用哪个数据库,完全看开发者的选择。中本聪最初发布的是 Bitcoin Core ,现在是作为实现 BTC 的一个参考,它使用的 LevelDB

Redis

使用 Redis 的原因是:

  1. 它很简单
  2. 只需要运行一个服务
  3. 它有持久化的实现
  4. 它也是类似于 LevelDB 的 key-value 结构

数据库结构

首先要决定好数据库的结构。参考 BTC 的做法:

  1. BTC 使用两个桶来存储数据
  2. 一个 blocks 用来存储区块,它存储了一条链中所有块的元数据(从创世块导最新区块)
  3. 另外一个 chainstate 用来存储链的状态,也就是记录所有 为花费交易输出(UTXO)区块索引

另外,处于性能的考虑, BTC 将每个区块存储为磁盘上的不同文件,这样,不需要为了读取一个块而将多个区块加载到内存中了。为了简单,这里不会实现这一点。

blocks 中, key -> value 的结构是:

KeyValue
b + 32-byte block hashBlock index record
l + 4-byte file numberLast block file number used
R + 1-byte booleanWhether reindexing is in progress
F + 1-byte flag name length + flag name stringVarious on/off flags (1-byte boolean: 0/1)
t + 32-byte transaction hashTransaction index record

chainstate 中, key -> value 的结构是:

KeyValue
c + 32-byte transaction hashUnspent Transaction Output (UTXO) record for the transaction
B + 32-byte block hashBlock hash up to which the UTXO set is current

详情

因为目前还没有交易,所以现在只有 blocks 桶。而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有:

  1. b -> 32 字节的 block-hash -> block 结构
  2. l -> 链中最后一个块的 hash

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

序列化

BTC 使用的 []byte 类型存储数据,因此这里也使用 []byte 类型。实现一个用于序列化的函数。

go
func (b *Block) Serialize() []byte {
	var result bytes.Buffer

	encoder := gob.NewEncoder(&result)
	err := encoder.Encode(b)
	if err != nil {
		panic(err)
	}

	return result.Bytes()
}

有了序列化,所以也需要一个反序列化。

go
func DeserializeBlock(b []byte) *Block {
	var block Block

	decoder := gob.NewDecoder(bytes.NewReader(b))
	err := decoder.Decode(&block)
	if err != nil {
		panic(err)
	}

	return &block
}

持久化

首先是 NewBlockchain 函数。这个函数会新建区块,因此,在这里写入数据库会比较好。大体流程是:

  1. 打开数据库
  2. 先判断是否存在区块
    1. 如果存在
      1. 那就创建新的区块,储到数据库
      2. 设置 Blockchain 的指针 tip 指向最新的区块
    2. 如果不存在
      1. 那就创建创世块,存储导数据库
      2. 让指针指 tip 向创世块
    3. 这里的 tip 的做法是存储最后一个块的哈希

由于不会在内存中存储所有的区块,只存储最新的区块,因此 Blockchain 的结构体需要修改:

go
type Blockchain struct {
	tip []byte
	rdb *redis.Client
	// Blocks []*Block
}

NewBlockchain 函数的修改为前面的逻辑:

go
const blocksBucket = "bucket:blocks:"

func NewBlockchain() *Blockchain {
	var tip []byte
	ctx := context.Background()

	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // 没有密码,默认值
		DB:       0,  // 默认是 0
	})

	keys, err := rdb.Keys(ctx, blocksBucket+"*").Result() // 获取桶中的数据
	if err != nil {
		log.Println("The databases is not data!")
	}

	if len(keys) == 0 { // 如果桶中没有数据
		fmt.Println("No existing blockchain found. Creating a new one...")
		genesis := NewGenesisBlock()
		rdb.Set(ctx, blocksBucket+string(genesis.Hash), genesis.Serialize(), 0)
		// 将创世块存入桶中,并且设置指针指向创世块
		rdb.Set(ctx, blocksBucket+string([]byte("l")), string(genesis.Hash), 0)
		// 在内存中存储创世块的哈希
		tip = genesis.Hash
	} else {
		// 获取指针的数据
		hash, err := rdb.Get(ctx, blocksBucket+string([]byte("l"))).Result()
		if err != nil {
			panic(err)
		}
		// 更新内存中的区块
		tip = []byte(hash)
	}

	bc := Blockchain{tip, rdb, ctx}
	return &bc
}

现在还有 AddBlock 方法需要修改,之前是向链中添加区块,现在需要改成存储到 Redis 中

go
func (bc *Blockchain) AddBlock(data string) {
	var prevBlock []byte

	// 获取前驱区块
	lastHashString, err := bc.rdb.Get(bc.ctx, blocksBucket+string([]byte("l"))).Result()
	if err != nil {
		panic(err)
	}
	prevBlock = []byte(lastHashString)

	// 生成当前区块
	newBlock := NewBlock(data, prevBlock)

	// 将最新的区块写入数据库
	bc.rdb.Set(bc.ctx, blocksBucket+string(newBlock.Hash), newBlock.Serialize(), 0)
	// 设置指针指向新区块
	bc.rdb.Set(bc.ctx, blocksBucket+string([]byte("l")), newBlock.Hash, 0)

	// 设置内存中存储最新的区块
	bc.tip = newBlock.Hash
}

至此,数据的持久化就完成了。

CLI

现在,程序从 main 函数中开始的,没有和用户交互的接口。现在来实现一个
所有的命令都会通过 CLI 结构体进行处理

go
type CLI struct {
	bc *Blockchain
}

它的入口是 Run 方法

go
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) verifyArgs() {
	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
		}
	}
}

func (cli *CLI) Run() {
	cli.verifyArgs()

	addBlockCmd := flag.NewFlagSet("ab", flag.ExitOnError)
	printChainCmd := flag.NewFlagSet("pc", flag.ExitOnError)

	addBlockData := addBlockCmd.String("data", "", "Block data")

	switch os.Args[1] {
	case "ab":
		err := addBlockCmd.Parse(os.Args[2:])
		if err != nil {
			panic(err)
		}
	case "pc":
		err := printChainCmd.Parse(os.Args[2:])
		if err != nil {
			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()
	}
}

这里还实现了一个 Iterator 以方便打印

go
type BlockchainIterator struct {
	currentHash []byte
	rdb         *redis.Client
	ctx         context.Context
}

func (bc *Blockchain) Iterator() *BlockchainIterator {
	bci := &BlockchainIterator{bc.tip, bc.rdb, context.Background()}

	return bci
}

func (i *BlockchainIterator) Next() *Block {
	var block *Block

	blockSerialize, err := i.rdb.Get(i.ctx, blocksBucket+string(i.currentHash)).Result()
	if err != nil {
		log.Println(err)
	}
	block = DeserializeBlock([]byte(blockSerialize))

	i.currentHash = block.PrevBlockHash

	return block
}

main 函数改成:

go
func main() {
	bc := blockchain.NewBlockchain()

	// bc.AddBlock("Send 1")
	// bc.AddBlock("Send 2")
	// bc.AddBlock("Send 3")

	// 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)
	// 	pow := blockchain.NewProofOfWork(block)
	// 	fmt.Printf("PoW: %t\n", pow.Validate())
	// 	fmt.Println()
	// }

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

电脑速度比较慢,将 PoW 的难度改成了 1 ,所以哈希的开头没有了 0 ,最后检验一下

go
go run blockchain.go pc
No existing blockchain found. Creating a new one...
Mining the block containing "Genesis Block"
043c3f96e3237e296c83fe67a01cbfc9bcd55c481fd7ed7b4aa632b068e25aaa

Prev hash: 
Data: Genesis Block
Hash: 043c3f96e3237e296c83fe67a01cbfc9bcd55c481fd7ed7b4aa632b068e25aaa
PoW: true


go run blockchain.go ab 
 -data "Send 1 BTC"     
Mining the block containing "Send 1 BTC"
4f5a202cf498fa20eba4f96e69411dc0190933a151884622e7f59cade97f2442

Success!

go run blockchain.go ab 
 -data "Send 2 BTC" 
Mining the block containing "Send 2 BTC"
1883c234e877f740cdfa4f943214d772044f41ffef7602fc68dde9bc6ce7fbd1

Success!

查询一下 Redis

127.0.0.1:6379> keys *
1) "bucket:blocks:l"
2) "bucket:blocks:\x04<?\x96\xe3#~)l\x83\xfeg\xa0\x1c\xbf\xc9\xbc\xd5\\H\x1f\xd7\xed{J\xa62\xb0h\xe2Z\xaa"
127.0.0.1:6379> keys *
1) "bucket:blocks:OZ ,\xf4\x98\xfa \xeb\xa4\xf9niA\x1d\xc0\x19\t3\xa1Q\x88F\"\xe7\xf5\x9c\xad\xe9\x7f$B"
2) "bucket:blocks:l"
3) "bucket:blocks:\x04<?\x96\xe3#~)l\x83\xfeg\xa0\x1c\xbf\xc9\xbc\xd5\\H\x1f\xd7\xed{J\xa62\xb0h\xe2Z\xaa"
4) "bucket:blocks:\x18\x83\xc24\xe8w\xf7@\xcd\xfaO\x942\x14\xd7r\x04OA\xff\xefv\x02\xfch\xdd\xe9\xbcl\xe7\xfb\xd1"