教你打造最简比特币—原型币之工作量证明

时间:2022-05-07
本文章向大家介绍教你打造最简比特币—原型币之工作量证明,主要内容包括工作量证明、算法实现、源码、下一节:、全系列:、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。
开发语言:Go语言

本教程是学习Jeiwan的博客后的学习笔记,代码实现也参考它的为主,精简了叙述并在适当位置添加了一些必备的小知识和适当的代码注释,如介绍哈希。

本教程是为了逐步教你设计一款简化的区块链原型币。通过我们不断添加功能,完成一个可交易的原型币。

本节增加基于工作量证明的区块产生机制

  1. 单机版,仅支持保存信息✅
  2. 工作量证明

工作量证明

工作量证明是一种区块的产生机制。产生一个区块不能随随便便,需要通过一番努力的工作,而完成工作的人,能得到相应的奖励。经过一番努力才能产生的区块,保证区块链的稳定和难以篡改的特性。

在比特币世界里,有一群按照工作量证明行事的矿工。他们就是一群提供网络、算力等计算机资源的人,他们的计算机不断按照比特币设计的工作量证明的规则计算数学谜题,解出谜题的人就得到了比特币的奖励,而大量的按照规则行事的诚实以期望获得奖励的比特币节点极大的保证了比特币网络的稳定,维持了它不可篡改的特性。

工作量证明的设计要满足两点,即工作量和证明。工作量是完成工作需要耗时耗力;证明是验证工作能非常快捷。这两点设计要求都可以通过哈希计算的算法实现。

上一张我们提到哈希有三个特性,其中的谜题友好(puzzle-friendly)特性,保证了从哈希值倒退原信息只能通过遍历的土方法,而没有任何的其他的启发式算法捷径。这就保证了耗时耗力。而反之,验证倒推出来的信息和这个哈希值又是非常快速的。

下面是哈希计算的土方法

  1. 取区块的头信息
  2. 取区块头中的一个计数器,称为nonce,
  3. 组合2和1的信息,计算哈希值。
  4. 检查哈希是否符合一定的条件
    1. 符合条件,结束
    2. 不符合,递增2的计数器,

这里的条件是,哈希的前几位数是0,要求越多的0,则计算难度越大。比特币通过区块产生的速度,动态调节这个0的多少,来保证每10分钟产生一个区块。

算法实现

首先定义,算法的检查条件,即计算难度

const targetBits = 24 //因为是单机版,不会动态调整,这里只是作为常量定义,不定义在区块头中

“target bits” 代表了难度,也就是开头有多少个 0,计算方法是,SHA256得到256bit的哈希值,转换为字符串表示是256/8*2,因为2个16进制的字符才表示一个byte,因此24/8*2=6个0字符串。

字符串例子如下:

0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3
0000010000000000000000000000000000000000000000000000000000000000
0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca

构建工作量证明的代码如下

type ProofOfWork struct {
    Block  *Block
    Target *big.Int  //big是go语言里的精确的大数的标准库
}

func NewProofOfWork(b *Block) *ProofOfWork {
    target := big.NewInt(1)
    target.Lsh(target, uint(256-targetBits))   //小于这个数的都是符合条件的哈希值

    pow := &ProofOfWork{b, target}

    return pow
}

区块里需要增加一个nonce计数器,

type Block struct {
    Timestamp     int64
    Data          []byte
    PrevHash []byte
    Hash          []byte
    Nonce         int
}

现在,我们需要有数据来进行哈希,准备数据:

func (pow *ProofOfWork) prepareData(nonce int) []byte {
    data := bytes.Join(
        [][]byte{
            pow.block.PrevHash,
            pow.block.Data,
            IntToHex(pow.Block.Timestamp),   //这里该用IntToHex,没关系,只要还原回来一致即可
            IntToHex(int64(targetBits)),    //这里的[]byte,是int数字代表的从数组序号大的一端写到小的一端。
            IntToHex(int64(nonce)),  //
        },
        []byte{},
    )

    return data
}

// IntToHex converts an int64 to a byte array
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()
}

现在就可以算法的实现:

func (pow *ProofOfWork) Run() (int, []byte) {
    var hashInt big.Int
    var hash [32]byte
    nonce := 0

    fmt.Printf("Mining the block containing "%s"n", pow.block.Data)
    for nonce < maxNonce {
        data := pow.prepareData(nonce)
        hash = sha256.Sum256(data)
        hashInt.SetBytes(hash[:])

        if hashInt.Cmp(pow.target) == -1 {
            fmt.Printf("r%x", hash)
            break
        } else {
            nonce++
        }
    }
    fmt.Print("nn")

    return nonce, hash[:]
}

成功后,我们就可以移除原来计算区块哈希的SetHash方法了,而且创建区块的NewBlock也需要引入工作量证明了。 修改如下

func NewBlock(data string, prevBlockHash []byte) *Block {
    block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, 0}
    pow := NewProofOfWork(block)
    nonce, hash := pow.Run()

    block.Hash = hash[:]
    block.Nonce = nonce

    return block
}

我们还要验证这个区块的哈希

func (pow *ProofOfWork) Validate() bool {
    var hashInt big.Int

    data := pow.prepareData(pow.block.Nonce)
    hash := sha256.Sum256(data)
    hashInt.SetBytes(hash[:])

    isValid := hashInt.Cmp(pow.target) == -1

    return isValid
}

最终,我们运行一下结果

func main() {
   bc := datastruct.GenesisBlockchain()

   bc.AddBlock("Send 1 BTC to Lin")
   bc.AddBlock("Send 2 BTC to Lin")

   for _, block := range bc.Blocks {
      fmt.Printf("Prev. hash: %xn", block.PrevHash)
      fmt.Printf("Data: %sn", block.Data)
      fmt.Printf("Hash: %xn", block.Hash)
      fmt.Println()

      pow := datastruct.NewProofOfWork(block)
      fmt.Printf("PoW: %sn", strconv.FormatBool(pow.Validate()))
      fmt.Println()
   }
}

输出:

Prev. hash:
Data: Genesis Block
Hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
PoW: true

Prev. hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
Data: Send 1 BTC to Ivan
Hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
PoW: true

Prev. hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
Data: Send 2 more BTC to Ivan
Hash: 000000e42afddf57a3daa11b43b2e0923f23e894f96d1f24bfd9b8d2d494c57a
PoW: true

参考

Building Blockchain in Go. Part 2: Proof-of-Work

源码

https://github.com/linxinzhe/go-simple-coin/tree/2proofof_work

下一节:

3. 区块链原型币—持久化

全系列:

  1. 区块链原形币--工作量证明
  2. 区块链原型币—工作量证明
  3. 区块链原型币—持久化
  4. 未完待续