持久化和命令行
现在有了 PoW ,可以挖矿了。现在需要将它存储起来,并且需要一个命令行的接口。
数据库
目前,区块链中没有用到数据库,每次运行程序会将数据库存储在内存中,程序一退出,所有的内容就都消失了。不能和别人共享了。
任何一个数据库都可以使用,在 BTC 的最初论文中,没有说具体要使用哪个数据库,完全看开发者的选择。中本聪最初发布的是 Bitcoin Core ,现在是作为实现 BTC 的一个参考,它使用的 LevelDB 。
Redis
使用 Redis 的原因是:
- 它很简单
- 只需要运行一个服务
- 它有持久化的实现
- 它也是类似于 LevelDB 的 key-value 结构
数据库结构
首先要决定好数据库的结构。参考 BTC 的做法:
BTC
使用两个桶来存储数据- 一个
blocks
用来存储区块,它存储了一条链中所有块的元数据(从创世块导最新区块) - 另外一个
chainstate
用来存储链的状态,也就是记录所有 为花费交易输出(UTXO) 和 区块索引
另外,处于性能的考虑, BTC 将每个区块存储为磁盘上的不同文件,这样,不需要为了读取一个块而将多个区块加载到内存中了。为了简单,这里不会实现这一点。
在 blocks
中, key -> value
的结构是:
Key | Value |
---|---|
b + 32-byte block hash | Block index record |
l + 4-byte file number | Last block file number used |
R + 1-byte boolean | Whether reindexing is in progress |
F + 1-byte flag name length + flag name string | Various on/off flags (1-byte boolean: 0/1) |
t + 32-byte transaction hash | Transaction index record |
在 chainstate
中, key -> value
的结构是:
Key | Value |
---|---|
c + 32-byte transaction hash | Unspent Transaction Output (UTXO) record for the transaction |
B + 32-byte block hash | Block hash up to which the UTXO set is current |
因为目前还没有交易,所以现在只有 blocks
桶。而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有:
- b -> 32 字节的 block-hash -> block 结构
- l -> 链中最后一个块的 hash
这就是实现持久化机制所有需要了解的内容了。
序列化
BTC 使用的 []byte
类型存储数据,因此这里也使用 []byte
类型。实现一个用于序列化的函数。
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()
}
有了序列化,所以也需要一个反序列化。
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
函数。这个函数会新建区块,因此,在这里写入数据库会比较好。大体流程是:
- 打开数据库
- 先判断是否存在区块
- 如果存在
- 那就创建新的区块,储到数据库
- 设置
Blockchain
的指针 tip 指向最新的区块
- 如果不存在
- 那就创建创世块,存储导数据库
- 让指针指 tip 向创世块
- 这里的 tip 的做法是存储最后一个块的哈希
- 如果存在
由于不会在内存中存储所有的区块,只存储最新的区块,因此 Blockchain
的结构体需要修改:
type Blockchain struct {
tip []byte
rdb *redis.Client
// Blocks []*Block
}
NewBlockchain
函数的修改为前面的逻辑:
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 中
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
结构体进行处理
type CLI struct {
bc *Blockchain
}
它的入口是 Run
方法
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 以方便打印
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
函数改成:
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 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"