摘要
本文构建了一个使用工作量证明机制 (POW) 的类 BTC 的区块链. 将区块链持久化到一个 Bolt 数据库中, 然后会提供一个简单的命令行接口, 用来完成一些与区块链的交互操作. 这篇文章目的是希望帮助大家理解 BTC 源码的架构, 所以主要专注于的实现原理及存储上, 暂时忽略了 "分布式" 这个部分. 严格意义上还不能算是一个完全意义上的区块链系统.
开发环境
语言: GO;
数据库: BoltDB;
IDE: Goland 或其他工具都可以;
系统: 不限, 本文使用 Windows.
BoltDB 数据库
实际上, 选择任何一个数据库都可以, 本文先用的是 BoltDB. 在比特币白皮书中, 并没有提到要使用哪一个具体的数据库, 它完全取决于开发者如何选择. 现在是比特币的一个参考实现, Bitcoin core 使用的是是 LevelDB.
BoltDB 安装及使用可以参考《BoltDB 简单使用教程》.
BoltDB 有如下优点:
非常简单和简约
用 Go 实现
不需要运行一个服务器
能够允许我们构造想要的数据结构
由于 Bolt 意在用于提供一些底层功能, 简洁便成为其关键所在. 它的 API 并不多, 并且仅关注值的获取和设置. 仅此而已.
Bolt 使用键值存储, 数据被存储为键值对(key-value pair, 就像 Golang 的 map). 键值对被存储在 bucket 中, 这是为了将相似的键值对进行分组(类似 RDBMS 中的表格). 因此, 为了获取一个值, 你需要知道一个 bucket 和一个键(key).
需要注意的一个事情是, Bolt 数据库没有数据类型: 键和值都是字节数组(byte array). 鉴于需要在里面存储 Go 的结构(准确来说, 也就是存储(块)Block), 我们需要对它们进行序列化, 也就说, 实现一个从 Go struct 转换到一个 byte array 的机制, 同时还可以从一个 byte array 再转换回 Go struct. 虽然我们将会使用 https://golang.org/pkg/encoding/gob/ 来完成这一目标, 但实际上也可以选择使用 JSON, xml, Protocol Buffers 等等. 之所以选择使用 encoding/gob, 是因为它很简单, 而且是 Go 标准库的一部分.
数据库结构
在开始实现持久化的逻辑之前, 我们首先需要决定到底要如何在数据库中进行存储. 为此, 我们可以参考 Bitcoin Core 的做法:
简单来说, Bitcoin Core 使用两个 "bucket" 来存储数据:
其中一个 bucket 是 blocks, 它存储了描述一条链中所有块的元数据
另一个 bucket 是 chainstate, 存储了一条链的状态, 也就是当前所有的未花费的交易输出, 和一些元数据
此外, 出于性能的考虑, Bitcoin Core 将每个区块 (block) 存储为磁盘上的不同文件. 如此一来, 就不需要仅仅为了读取一个单一的块而将所有 (或者部分) 的块都加载到内存中. 但是, 为了简单起见, 我们并不会实现这一点.
在 blocks 中, key -> value 为:
key | value |
---|---|
b + 32 字节的 block hash | block index record |
f + 4 字节的 file number | file information record |
l + 4 字节的 file number | the last block file number used |
R + 1 字节的 boolean | 是否正在 reindex |
F + 1 字节的 flag name length + flag name string | 1 byte boolean: various flags that can be on or off |
t + 32 字节的 transaction hash | transaction index record |
在 chainstate,key -> value 为:
key | value |
---|---|
c + 32 字节的 transaction hash | unspent transaction output record for that transaction |
B | 32 字节的 block hash: the block hash up to which the database represents the unspent transaction outputs |
因为目前还没有交易, 所以我们只需要 blocks bucket. 另外, 正如上面提到的, 我们会将整个数据库存储为单个文件, 而不是将区块存储在不同的文件中. 所以, 我们也不会需要文件编号 (file number) 相关的东西. 最终, 我们会用到的键值对有:
32 字节的 block-hash -> block 结构
l -> 链中最后一个块的 hash
这就是实现持久化机制所有需要了解的内容了.
系统实现
1. 区块文件 block.go
该部分主要包括:
对区块结构的定义; 创建区块的方法 NewBlock(); 区块的序列化 Serialize()与反序列化 Deserialize()函数; 以及创世区块的生成 NewGenesisBlock().
- // 定义一个区块的结构 Block
- type Block struct {
- // 版本号
- Version int64
- // 父区块头哈希值
- PreBlockHash []byte
- // 当前区块的 Hash 值, 为了简化代码
- Hash []byte
- //Merkle 根
- MerkleRoot []byte
- // 时间戳
- TimeStamp int64
- // 难度值
- Bits int64
- // 随机值
- Nonce int64
- // 交易信息
- Data []byte
- }
- // 提供一个创建区块的方法
- func NewBlock(data string, preBlockHash []byte) *Block {
- var block Block
- block = Block{
- Version: 1,
- PreBlockHash: preBlockHash,
- //Hash TODO
- MerkleRoot: []byte{},
- TimeStamp: time.Now().Unix(),
- Bits: targetBits,
- Nonce: 0,
- Data: []byte(data)}
- //block.SetHash()
- pow := NewProofOfWork(&block)
- nonce, hash := pow.Run()
- block.Nonce = nonce
- block.Hash = hash
- return &block
- }
- // 将 Block 序列化为一个字节数组
- func (block *Block) Serialize() []byte {
- var buffer bytes.Buffer
- encoder := gob.NewEncoder(&buffer)
- err := encoder.Encode(block)
- CheckErr("Serialize", err)
- return buffer.Bytes()
- }
- // 将字节数组反序列化为一个 Block
- func Deserialize(data []byte) *Block {
- if len(data) == 0 {
- return nil
- }
- var block Block
- decoder := gob.NewDecoder(bytes.NewReader(data))
- err := decoder.Decode(&block)
- CheckErr("Deserialize", err)
- return &block
- }
- // 创世块
- func NewGenesisBlock() *Block {
- return NewBlock("Genesis Block", []byte{})
- }
2. 区块链 blockChain.go
该部分内容主要包括:
定义一个区块链结构 BlockChain 结构体;
提供一个创建 BlockChain 的方法 NewBlockChain();
提供一个添加区块的方法 AddBlock(data string);
迭代器对区块进行遍历.
- const dbFile = "blockchain.db"
- const blocksBucket = "bucket"
- const lastHashKey = "key"
- // 定义一个区块链结构 BlockChain
- type BlockChain struct {
- //blocks []*Block
- // 数据库的操作句柄
- db *bolt.DB
- //tail 尾巴, 表示最后一个区块的哈希值
- // 在链的末端可能出现短暂分叉的情况, 所以选择 tail 其实也就是选择了哪条链
- tail []byte
- }
- // 提供一个创建 BlockChain 的方法
- func NewBlockChain() *BlockChain {
- // 打开一个 BoltDB 文件
- //func Open(path string, mode os.FileMode, options *Options) (*DB, error)
- db, err := bolt.Open(dbFile, 0600, nil)
- //utils 中的校验函数, 校验错误
- CheckErr("NewBlockChain1", err)
- var lastHash []byte
- err = db.Update(func(tx *bolt.Tx) error {
- bucket := tx.Bucket([]byte(blocksBucket))
- // 如果数据库中不存在 bucket, 要去创建创世区块, 将数据填写到数据库的 bucket 中
- if bucket == nil {
- fmt.Println("No existing blockchain found. Creating a new one...")
- genesis := NewGenesisBlock()
- bucket, err := tx.CreateBucket([]byte(blocksBucket))
- CheckErr("NewBlockChain2", err)
- err = bucket.Put(genesis.Hash, genesis.Serialize())
- CheckErr("NewBlockChain3", err)
- err = bucket.Put([]byte(lastHashKey), genesis.Hash)
- CheckErr("NewBlockChain4", err)
- lastHash = genesis.Hash
- } else {
- // 直接读取最后区块的哈希值
- lastHash = bucket.Get([]byte(lastHashKey))
- }
- return nil
- })
- CheckErr("db.Update", err)
- return &BlockChain{db, lastHash}
- }
- // 提供一个添加区块的方法
- func (bc *BlockChain) AddBlock(data string) {
- var preBlockHash []byte
- err := bc.db.View(func(tx *bolt.Tx) error {
- bucket := tx.Bucket([]byte(blocksBucket))
- if bucket == nil {
- os.Exit(1)
- }
- preBlockHash = bucket.Get([]byte(lastHashKey))
- return nil
- })
- CheckErr("AddBlock-View", err)
- block := NewBlock(data, preBlockHash)
- err = bc.db.Update(func(tx *bolt.Tx) error {
- bucket := tx.Bucket([]byte(blocksBucket))
- if bucket == nil {
- os.Exit(1)
- }
- err = bucket.Put(block.Hash, block.Serialize())
- CheckErr("AddBlock1", err)
- err = bucket.Put([]byte(lastHashKey), block.Hash)
- CheckErr("AddBlock2", err)
- bc.tail = block.Hash
- return nil
- })
- CheckErr("AddBlock-Update", err)
- }
- // 迭代器, 就是一个对象, 它里面包含了一个游标, 一直向前 / 后移动, 完成整个容器的遍历
- type BlockChainIterator struct {
- currentHash []byte
- db *bolt.DB
- }
- // 创建迭代器, 同时初始化为指向最后一个区块
- func (bc *BlockChain) NewIterator() *BlockChainIterator {
- return &BlockChainIterator{bc.tail, bc.db}
- }
- // 返回链中的下一个块
- func (it *BlockChainIterator) Next() (block *Block) {
- err := it.db.View(func(tx *bolt.Tx) error {
- bucket := tx.Bucket([]byte(blocksBucket))
- if bucket == nil {
- return nil
- }
- data := bucket.Get(it.currentHash)
- block = Deserialize(data)
- it.currentHash = block.PreBlockHash
- return nil
- })
- CheckErr("Next", err)
- return
- }
3. 工作量证明机制 POW.go
该部分主要包括:
创建 POW 的方法 NewProofOfWork(block *Block) ;
计算哈希值的方法 Run() (int64, []byte);
- // 定义一个工作量证明的结构 ProofOfWork
- type ProofOfWork struct {
- block *Block
- // 目标值
- target *big.Int
- }
- // 难度值常量
- const targetBits = 20
- // 创建 POW 的方法
- func NewProofOfWork(block *Block) *ProofOfWork {
- //000000000000000... 01
- target := big.NewInt(1)
- //0x1000000000000...00
- target.Lsh(target, uint(256-targetBits))
- pow := ProofOfWork{block: block, target: target}
- return &pow
- }
- // 给 Run()准备数据
- func (pow *ProofOfWork) PrepareData(nonce int64) []byte {
- block := pow.block
- tmp := [][]byte{
- /*
- 需要将 block 中的不同类型都转化为 byte, 以便进行连接
- */
- IntToByte(block.Version),
- block.PreBlockHash,
- block.MerkleRoot,
- IntToByte(block.TimeStamp),
- IntToByte(nonce),
- IntToByte(targetBits),
- block.Data}
- //func Join(s [][]byte, sep []byte) []byte
- data := bytes.Join(tmp, []byte{})
- return data
- }
- // 计算哈希值的方法
- func (pow *ProofOfWork) Run() (int64, []byte) {
- /* 伪代码
- for nonce {
- hash := sha256(block 数据 + nonce)
- if 转换(Hash)<pow.target{
- 找到了
- }else{
- nonce++
- }
- }
- return nonce,hash{:}
- */
- //1. 拼装数据
- //2. 哈希值转成 big.Int 类型
- var hash [32]byte
- var nonce int64 = 0
- var hashInt big.Int
- fmt.Println("Begin Minding...")
- fmt.Printf("target hash : %x\n", pow.target.Bytes())
- for nonce < math.MaxInt64 {
- data := pow.PrepareData(nonce)
- hash = sha256.Sum256(data)
- hashInt.SetBytes(hash[:])
- // Cmp compares x and y and returns:
- //
- // -1 if x < y
- // 0 if x == y
- // +1 if x> y
- //
- //func (x *Int) Cmp(y *Int) (r int) {
- if hashInt.Cmp(pow.target) == -1 {
- fmt.Printf("found hash :%x,nonce :%d\n,", hash, nonce)
- break
- } else {
- //fmt.Printf("not found nonce,current nonce :%d,hash : %x\n", nonce, hash)
- nonce++
- }
- }
- return nonce, hash[:]
- }
- // 校验函数
- func (pow *ProofOfWork) IsValid() bool {
- var hashInt big.Int
- data := pow.PrepareData(pow.block.Nonce)
- hash := sha256.Sum256(data)
- hashInt.SetBytes(hash[:])
- return hashInt.Cmp(pow.target) == -1
- }
4. 命令函交互 CLI.go
注意这部分需要使用标准库里面的 https://golang.org/pkg/flag/ 包来解析命令行参数;
首先, 创建两个子命令: addblock 和 printchain, 然后给 addblock 添加 --data 标志. printchain 没有标志;
然后, 检查用户输入的命令并解析相关的 flag 子命令;
最后检查解析是哪一个子命令, 并调用相关函数执行.
具体如下:
- // 因为是多行的, 所以用反引号 `...` 包一下, 可以实现多行字符串的拼接, 不需要转义!
- // 命令行提示
- const usage = `
- Usage:
- addBlock -data BLOCK_DATA "add a block to the blockchain"
- printChain "print all the blocks of the blockchain"
- `
- const AddBlockCmdString = "addBlock"
- const PrintChainCmdString = "printChain"
- // 输出提示函数
- func (cli *CLI) printUsage() {
- fmt.Println("Invalid input!")
- fmt.Println(usage)
- os.Exit(1)
- }
- // 参数检查函数
- func (cli *CLI) validateArgs() {
- if len(os.Args) < 2 {
- fmt.Println("invalid input!")
- cli.printUsage()
- }
- }
- func (cli *CLI) Run() {
- cli.validateArgs()
- addBlockCmd := flag.NewFlagSet(AddBlockCmdString, flag.ExitOnError)
- printChainCmd := flag.NewFlagSet(PrintChainCmdString, flag.ExitOnError)
- //func (f *FlagSet) String(name string, value string, usage string) *string
- addBlocCmdPara := addBlockCmd.String("data", "","Block data")
- switch os.Args[1] {
- case AddBlockCmdString:
- // 添加动作
- err := addBlockCmd.Parse(os.Args[2:])
- CheckErr("Run()1", err)
- if addBlockCmd.Parsed() {
- if *addBlocCmdPara == "" {
- fmt.Println("addBlock data not should be empty!")
- cli.printUsage()
- }
- cli.AddBlock(*addBlocCmdPara)
- }
- case PrintChainCmdString:
- // 打印输出
- err := printChainCmd.Parse(os.Args[2:])
- CheckErr("Run()2", err)
- if printChainCmd.Parsed() {
- cli.PrintChain()
- }
- default:
- // 命令不符合规定, 输出提示信息
- cli.printUsage()
- }
- }
区块链操作演示效果:
首先 go build 编译程序; 输入不带 --data 参数的错误命令, 查看提示.
输入交易信息, 查看 pow 运算:
打印区块链已有区块信息:
来源: https://www.cnblogs.com/X-knight/p/10759493.html