zoukankan      html  css  js  c++  java
  • [区块链理解BTCD源码]GO语言实现一个区块链原型

    摘要

    本文构建了一个使用工作量证明机制(POW)的类BTC的区块链。将区块链持久化到一个Bolt数据库中,然后会提供一个简单的命令行接口,用来完成一些与区块链的交互操作。这篇文章目的是希望帮助大家理解BTC源码的架构,所以主要专注于的实现原理及存储上,暂时忽略了 “分布式” 这个部分。严格来说还不能算是一个完全意义上的区块链系统。

    开发环境

    语言:GO;

    数据库:BoltDB;

    IDE: Goland或其他工具都可以;

    系统:不限,本文使用windows。

    BoltDB数据库

    实际上,选择任何一个数据库都可以,本文先用的是BoltDB。在比特币白皮书中,并没有提到要使用哪一个具体的数据库,它完全取决于开发者如何选择。现在是比特币的一个参考实现,Bitcoin core使用的是是LevelDB。

    BoltDB安装及使用可以参考《BoltDB简单使用教程》

    BoltDB有如下优点:

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

    由于 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。虽然我们将会使用 encoding/gob 来完成这一目标,但实际上也可以选择使用 JSON, XML, Protocol Buffers 等等。之所以选择使用 encoding/gob, 是因为它很简单,而且是 Go 标准库的一部分。

    区块链原型的函数架构

    系统实现

    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()

    我们希望NewBlockchain实现的功能有

    1. 打开一个数据库文件
    2. 检查文件里面是否已经存储了一个区块链
    3. 如果已经存储了一个区块链:
      1. 创建一个新的 Blockchain 实例
      2. 设置 Blockchain 实例的 tip 为数据库中存储的最后一个块的哈希
    4. 如果没有区块链:
      1. 创建创世块
      2. 存储到数据库
      3. 将创世块哈希保存为最后一个块的哈希
      4. 创建一个新的 Blockchain 实例,初始时 tail 指向创世块( tail存储的是最后一个块的哈希值)
    • 提供一个添加区块的方法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
    ", 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
    ,", hash, nonce)
                break
            } else {
                //fmt.Printf("not found nonce,current nonce :%d,hash : %x
    ", 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

    注意这部分需要使用标准库里面的 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运算:

    打印区块链已有区块信息:

    Reference:

    最后要感谢Ivan Kuznetsov在GitHub社区的贡献!

  • 相关阅读:
    使用InstelliJ IDEA创建Web应用程序
    别了WindowsXP
    在MyEclipse中搭建Spring MVC开发环境
    iPhone中国移动收不到彩信,联通不用设置都可以,具体设置方法:
    WebLogic 服务器配置
    c# 第五课 string
    c# 第五课 regex
    c# 第四课 Arrays
    c# 第四课 interfaces
    c# 第五课 async await
  • 原文地址:https://www.cnblogs.com/X-knight/p/10759493.html
Copyright © 2011-2022 走看看