实验二:使用Go语言构造区块链

实验概述

21 世纪最具先峰性的代表性技术之一,就是区块链。目前,它仍然处于,并将长期处于不断成长的时期,而且,在他的身上,还有很多潜在的力量,没有完全展露出来。从本质上来讲,区块链的核心,可以说是一个分布式数据库而已。不过,在区块链中,与传统的分布式数据库,最为独一无二的地方在于,区块链的数据库是公开的,而不是一个私人数据库。也就是说,每个使用它的人,都将在自己的机器上,拥有一个或部分,或完整的副本。而向数据库中添加新的记录,必须经过其他“矿工”的同意,才可以。除此以外,也是因为区块链的兴起,才使得加密货币和智能合约这一新兴技术,成为正在发生的事情。

本实验将在Go语言的环境下,实现一个简化版的区块链。

实验目标

  1. 熟练掌握用Go语言的语法。

  2. 在实践中学会构造区块链的区块(实验2-1),将区块链接为链(实验2-2),为该区块链添加工作量证明(实验2-3),

  3. 举一反三,发散思维,尝试实现链上数据的持久化存储(拓****展实验2-4) ,为该区块链添加命令行接口(拓展实验2-5)。

实验内容

实验2-1:构建区块

将ex2-1文件夹下的blockchain_demo文件夹下的代码补充完整:

  • Block类结构体
    1
    2
    3
    4
    5
    6
    7
    type Block struct {
    Time int64
    Nonce int64
    PrevHash []byte
    Hash []byte
    Data []byte
    }
  • Block.SetHash(),实现对Block的Hash计算
    1
    2
    3
    4
    5
    func (b *Block) SetHash() {
    montage := append(b.PrevHash, IntToHex(b.Time)...)
    montage = append(montage, b.Data...)
    b.Hash = calHash(montage)
    }
  • NewBlock(),创建新区块
    1
    2
    3
    4
    5
    func NewBlock(data string, prevHash []byte) *Block {
    block := &Block{time.Now().Unix(), 0, prevHash, []byte{}, []byte(data)}
    block.SetHash()
    return block
    }

运行结果:

image-20230928174059043

实验2-2:实现一条链

补全blockchain.go文件:

  • 添加区块函数Blockchain.AddBlock()
    1
    2
    3
    4
    5
    6
    7
    8
    func (bc *Blockchain) AddBlock(data string) {
    //可能用到的函数:
    // len(array):获取数组长度
    lenBC := len(bc.blocks)
    lastBlock := NewBlock(data, bc.blocks[lenBC-1].Hash)
    // append(array,b):将元素b添加至数组array末尾
    bc.blocks = append(bc.blocks, lastBlock)
    }
  • 创世区块生成函数GenesisBlock()

    1
    2
    3
    4
    5
    func NewGenesisBlock() *Block {
    //创世区块前置哈希为空,Data为"Genesis Block"
    block := NewBlock("Genesis Block", []byte{})
    return block
    }
  • 修改main函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func main() {
    t := time.Now()
    bc := NewBlockchain()
    bc.AddBlock("Send 1 BTC to Ivan")
    bc.AddBlock("Send 2 more BTC to Ivan")
    for _, block := range bc.blocks {
    fmt.Printf("PrevHash: %x\n", block.PrevHash)
    fmt.Printf("Data: %s\n", block.Data)
    fmt.Printf("Hash: %x\n", block.Hash)
    fmt.Println()
    }
    fmt.Println("Time using: ", time.Since(t))
    }

    运行结果:

    image-20230928174501094

实验2-3:添加工作量证明模块

补全ProofOfWork.go文件

  • ProofOfWork.Mine()函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    func (pow *ProofOfWork) Mine() (int64, []byte) {
    var hashInt big.Int
    var hash []byte
    nonce := int64(0)
    fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)
    // 寻找符合条件的hash
    for {
    hash = calHash(pow.prepareData(nonce))
    hashInt.SetBytes(hash)
    cmp := hashInt.Cmp(pow.target)
    if cmp < 0 {
    break
    }
    nonce += 1
    }
    fmt.Printf("\r%x", hash)
    fmt.Printf("\n\n")
    return nonce, hash[:]
    }
  • ProofOfWork.Validate()函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    func (pow *ProofOfWork) Validate() bool {
    var hashInt big.Int
    var isVaild = false
    // 检查hash正确性
    hash := calHash(pow.prepareData(pow.block.Nonce))
    if bytes.Compare(hash, pow.block.Hash) != 0 {
    return false
    }
    // 检查hash满足target
    hashInt.SetBytes(hash)
    cmp := hashInt.Cmp(pow.target)
    if cmp < 0 {
    isVaild = true
    }
    return isVaild
    }
  • 修改Block类,使其哈希计算方法变为工作量证明算法在main函数中添加对区块哈希的PoW验证
    1
    2
    3
    4
    5
    // SetHash 通过PoW计算区块hash
    func (b *Block) SetHash() {
    pow := NewProofOfWork(b)
    pow.block.Nonce, pow.block.Hash = pow.Mine()
    }

回答问题:工作量证明中的difficulty值的大小会怎样影响PoW计算时间?

  • difficulty越大,PoW计算时间越长,2n2^n的指数形式复杂度
  • 具体数据测试如下表

    | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | avg | |
    | ------ | ------- | ------ | ------- | ------ | ------ | ------- | ------- | ------- | ------ | ------- | ---- |
    | 565.84 | 1127.76 | 976.77 | 1377.62 | 911.12 | 951.12 | 1295.62 | 1769.02 | 1320.01 | 827.29 | 1112.22 | ms |
    | 21.37 | 36.29 | 14.41 | 20.65 | 22.69 | 14.24 | 36.19 | 3.06 | 8.99 | 24.61 | 20.25 | s |

运行结果:

image-20230928175935446

扩展实验2-4:阅读代码,添加数据库

将程序调通:

  • 修改Block结构:增添Nonce(必须大写,否则无法序列化,巨坑!!!)
    1
    2
    3
    4
    5
    6
    7
    type Block struct {
    Time int64
    Nonce int64
    PrevHash []byte
    Hash []byte
    Data []byte
    }
  • 增添序列化和反序列化函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // Serialize 序列化:block->[]byte
    func (b *Block) Serialize() []byte {
    var result bytes.Buffer
    encoder := gob.NewEncoder(&result)
    err := encoder.Encode(b)
    if err != nil {
    return nil
    }
    return result.Bytes()
    }

    // DeserializeBlock 反序列化:[]byte->block
    func DeserializeBlock(d []byte) *Block {
    var block Block
    decoder := gob.NewDecoder(bytes.NewReader(d))
    err := decoder.Decode(&block)
    if err != nil {
    return nil
    }
    return &block
    }
  • 修改main函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func main() {
    t := time.Now()
    bc := NewBlockchain()
    bc.AddBlock("Send 1 BTC to Ivan")
    bc.AddBlock("Send 2 more BTC to Ivan")
    bci := bc.Iterator()
    for {
    block := bci.Next()
    fmt.Printf("PrevHash: %x\n", block.PrevHash)
    fmt.Printf("Data: %s\n", block.Data)
    fmt.Printf("Hash: %x\n", block.Hash)
    pow := NewProofOfWork(block)
    fmt.Printf("PoW: %t\n", pow.Validate())
    fmt.Println()
    if len(block.PrevHash) == 0 {
    break
    }
    }
    fmt.Println("Time using: ", time.Since(t))
    }

运行结果:

image-20230928180539248

回答问题:

  • 为什么需要在block类中添加Serialize()和DeserializeBlock()两个函数?他们主要做了什么? 描述一下NewBlockchain()和NewBlock()的执行逻辑

    • 区块链信息在代码中是以结构体的形式存在的,但是存储到数据库则只能用[]byte形式。所以在存储和读取的过程中,序列化和反序列化是必不可少的。
    • 二者进行struct和[]byte两种形式的转换
    • NewBlockchain()执行逻辑:
      1. 打开数据库文件,进行错误处理
      2. 检查是否存在一个区块链:
        • 如果存在:
          • 对其创建Blockchain 实例,将 Blockchain中的tip设置为从数据库key l读取到的最后一个区块hash
        • 如果不存在:
          1. 创建创世区块
          2. 存储至数据库
          3. 把key l对应的value设为创世区块的hash
          4. 将上述区块链创建 Blockchain 实例,设置tip为创世区块的hash
      • NewBlock()执行逻辑:
        1. 取出数据库key l,读取最后一个区块hash
        2. 根据末尾区块hash,计算并创建新区块
        3. 将新区块序列化后存入数据库
        4. 更新数据库key l的value为新区块hash
    • Blockchain类中的tip变量是做什么用的?
      • 在迭代器中,作为存放当前读取区块hash的索引变量,或者说做一个标记记录,避免key l被改变or使用
    • 迭代器Interator是如何工作使得我们能够从数据库中遍历出区块信息的?
      • 从tip变量开始,通过取出最新区块,并更新pre区块,重新标记hash,可以倒序遍历所有区块。

    扩展实验2-5:添加命令行接口

编写命令行参数文件arg.go:

  • InitArg()函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func InitArgs() {
    args := os.Args[1:]
    flagadd := flag.NewFlagSet("add", flag.ExitOnError)
    addData := flagadd.String("data", "nil", "添加数据区块")
    if len(args) < 1 {
    fmt.Println("expected subcommands")
    os.Exit(1)
    }
    bc := NewBlockchain()
    switch args[0] {
    case "list":
    bc.ListBlock()
    case "add":
    err := flagadd.Parse(args[1:])
    if err != nil {
    return
    }
    bc.AddBlock(*addData)
    }
    }

修改main函数:

  •   func main() {
          t := time.Now()
          InitArgs()
          fmt.Println("Time using: ", time.Since(t))
      }
      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    #### 运行结果:

    ### ![image-20230928192052424](ex2/image-20230928192052424.png)

    ![image-20230928192138537](ex2/image-20230928192138537.png)

    ![image-20230928192201070](ex2/image-20230928192201070.png)

    ### 附录:工具函数

    #### utils.go

    ##### Int64 to []byte(hex)

    + ```go
    func IntToHex(num int64) []byte {
    buff := new(bytes.Buffer)
    err := binary.Write(buff, binary.BigEndian, num)
    if err != nil {
    log.Panic(err)
    }

    return buff.Bytes()
    }
计算sha256
  •   func calHash(inputData []byte) []byte {
      	sha256Example := sha256.New()
      	sha256Example.Write(inputData)
      	sha256Result := sha256Example.Sum(nil)
      	return sha256Result
      }