使用 golang 实现一个极简的 区块链

2,146 阅读11分钟

背景: 最近在学习区块链相关知识, 主要参考了 hackernoon.com/learn-block… 这个教程, 教程中使用了 python 实现了一个简单的区块链。因为 golang 是区块链的主流语言,于是我在彻底理解了相关概念后使用 golang 又实现了一遍,并且对里面的少部分代码逻辑进行了重构显得更加清晰明了。看完此文,你将彻底理解区块链的关键特性包括; 不可篡改性,如何往区块链中添加交易信息, 如何实现工作量证明, 如何记账(挖矿),如何实现多节点一致性, 如何去中心化,并且使用golang从0实现一个区块链。 希望你有密码学的基础知识, 和一致性算法的基础知识。如果没有,可以参考我的另外两篇博客 Raft 算法选主详解与复现, 完成 MIT 6.824(6.5840) Lab2A看完符合安全厂商标准的密码体系,再看看你公司的密码体系够安全吗

前置知识

区块链是什么?

1712940858638.png

就像上面这幅图一样,区块链是一个个块组成的链。块称作 Block, Block 形成的链称作 Block Chain. Block 里面大概有这几种数据:

  1. index, 表示区块链的索引,是第几个块
  2. timestamp, 生成这个块的时间
  3. transaction, 这个块里面的交易信息,这里是一个list, 因为一个块里面可能会有多个交易信息, transaction 里面又有3部分: sender 表示转账方, recipient 表示接收方, amount 表示金额。 transaction 也被称作是账本, 是整个块中真正有价值的信息
  4. pow, proof of work, 工作量证明, 谁先算出这个 pow, 谁就有资格对这个块进行打包。 计算 pow 然后打包 的这个过程也称作记账, 记账完成后有奖励。为了获取奖励而进行记账,这就叫做挖矿
  5. prevHash, 对前一个块的所有内容进行hash

上面这么讲,是比较偏向于从数据结构方面进行理解,下面来分析下这种数据结构可以推导出 区块链的什么性质

区块链性质

为什么要记账(挖矿)

再重复一次,记账也就是对交易信息进行打包。在区块链中,每一个节点手上都一个链,当一个节点发生交易的时候,这个节点就会对周围节点进行广播,收到广播后的节点就可以对这笔交易进行打包。为什么要打包呢? 因为打包有奖励? 为什么又奖励呢? 因为打包过程需要消耗计算资源,通常是一个很复杂的密码学操作。通过解决密码学难题,从而竞争获得唯一的记账权。 一旦某个节点记账成功 ,其他节点则复制这个结果。

不可篡改性

不可篡改性可以从两方面来解释:

链路的逐次哈希过程

因为每一个块中都有 上一个 哈希值, 所以你要想改变中间的某一个数据,那么后面的所有哈希值都会对不上,别的节点一旦对你的链路进行交易就会发现你的块是被篡改的

链路的逐次工作量证明

每个块的工作量证明都是和上一个块的哈希有关的,如果某个节点想篡改中间的某个交易,那么这个块的以及这个块后面哈希都会变,这个块和这个块后面的所有 工作量证明就会作废,需要重新计算工作量证明。前面说过这个工作量证明是一个非常消耗计算资源的密码学难题的,对一个区块打包都非常困难,更何况对篡改的块之后所有的块进行打包

一致性维护

在区块链中每个节点都保存一个区块链,那么如何维护所有节点的一致性呢? 也很简单,所有节点会以最长的那条链为准。这就好比 raft 算法的选主过程, 所有节点都会给 term 最大的节点投票, 所有节点都听 leader 发号施令类似。

去中心化

基于上面的分布式区块链来说,所有节点的地位是一样的,并不存在某个节点可以控制其他的节点的情况。挖矿完成的节点可以看作是一个临时的leader, 大家都以它的最长链路为准,但这个节点的权限相对于中心化的系统来说也是非常小的。

用 golang 实现一个区块链

接下来,我将使用 golang 从0实现一个区块链,包括了以下功能

  1. 区块链的基本组成部分实现
  2. 往链中添加交易,只添加交易,但不进行打包
  3. 对交易进行打包,工作量证明计算, 获取奖励,对外来说就是挖矿
  4. 往当前节点中注册其他节点
  5. 一致性校验,往其他节点发送请求以获取其他节点的区块链,然后对其有效性进行校验,若通过校验且其他节点的链更长,则更新自己区块链

创建工程目录

创建 block_chain_demo 文件夹,命令行输入:

go mod block_chain_demo
go mod tidy

文件夹目录如下:

block_chain_demo
    block_chain
        -- block_chain // 区块链代码
    main
        -- main.go // main 函数 以及 调用区块链的 http 接口
    -- go.mod

实现区块链

block_chain.go

在 block_chain.go 文件我们定义了区块链的所有操作

BlockChain 成员

package block_chain

import (
   "crypto/sha256"
   "encoding/hex"
   "encoding/json"
   "fmt"
   "io"
   "net/http"
   "strconv"
   "time"
)

type BlockChain struct {
   NodeIdentifier      string
   Chain               []Block
   Nodes               []string
   CurrentTransactions []Transaction
}

type Transaction struct {
   Sender    string `json:"sender"`
   Recipient string `json:"recipient"`
   Amount    uint   `json:"amount"`
}

type Block struct {
   Index        int           `json:"index"`
   TimeStamp    int64         `json:"timeStamp"`
   Transactions []Transaction `json:"transactions"`
   Pow          uint64        `json:"pow"`
   PrevHash     string        `json:"prevHash"`
}


在BlcokChain结构体中定义了区块链中所有的成员属性,包括:

  1. NodeIdentifier 表示当前链的id
  2. Chain, 区块链本体, 由一个个block组成
  3. Nodes, 当前区块链节点的邻居节点集合
  4. CurrentTransaction, 当前区块链中未打包的交易信息

剩下的 Transaction 和 Block 在前面的章节已经讲过了,这里不再赘述

BlcokChain 方法


// constructor
func (b *BlockChain) Init(nodeIdentifier string) {
   b.Nodes = []string{}

   firstBlock := Block{
      Index:        0,
      TimeStamp:    time.Now().Unix(),
      Transactions: nil,
      Pow:          1,
      PrevHash:     "1",
   }
   b.Chain = append(b.Chain, firstBlock)

}

// register node
func (b *BlockChain) RegisterNode(node string) {
   b.Nodes = append(b.Nodes, node)
}

// get nodes
func (b *BlockChain) GetNodes() []string {
   return b.Nodes
}

// get block hash
func (b *BlockChain) GetBlockHash(block Block) string {
   blockBytes, _ := json.Marshal(block)
   prevHashValueTmp := sha256.Sum256(blockBytes)
   return hex.EncodeToString(prevHashValueTmp[:])

}

// verify chain validity
func (b *BlockChain) VerifyChain(chain []Block) bool {

   if len(chain) <= 1 {
      return true
   }

   lastBlcok := chain[0]
   curChainIdx := 1

   for curChainIdx < len(chain) {
      curBlcok := chain[curChainIdx]

      // verify current block with last block by hash function
      if curBlcok.PrevHash != b.GetBlockHash(lastBlcok) {
         return false
      }

      // verify pow of current block
      if !b.VerifyPOW(lastBlcok.Pow, curBlcok.Pow) {
         return false
      }
      lastBlcok = curBlcok
      curChainIdx++

   }

   return true

}

type FullChainResp struct {
   Chain        []Block       `json:"chain"`
   Len          int           `json:"len"`
   Transactions []Transaction `json:"transactions"`
   Nodes        []string      `json:"nodes"`
}

// resolve conflicts
func (b *BlockChain) ResolveConflicts() {

   var maxLenChain []Block
   var maxLen = len(b.Chain)

   // send request to neighbour nodes, and verify their chain
   for _, node := range b.Nodes {
      resp, err := http.Get(node + "/fullChain")
      if err != nil {
         fmt.Println(err)
         continue
      }

      defer resp.Body.Close()

      fullChainRespBytes, err := io.ReadAll(resp.Body)
      if err != nil {
         fmt.Println("Failed to read response body:", err)
         return
      }

      var fullChainResp FullChainResp
      err = json.Unmarshal(fullChainRespBytes, &fullChainResp)
      if err != nil {
         fmt.Println("Failed to read response body:", err)
         return
      }

      if len(fullChainResp.Chain) != fullChainResp.Len {
         continue
      }

      if fullChainResp.Len > maxLen && b.VerifyChain(fullChainResp.Chain) {
         maxLen = fullChainResp.Len
         maxLenChain = fullChainResp.Chain
      }

   }

   // if find a longer chain, update
   if maxLen > len(b.Chain) {
      b.Chain = maxLenChain
   }

}

// add a new transaction, need to be mined into a new block
func (b *BlockChain) NewTransaction(newTransaction Transaction) {
   b.CurrentTransactions = append(b.CurrentTransactions, newTransaction)
}

// get transaction size
func (b *BlockChain) GetCurrentTransactionsSize() int {
   return len(b.CurrentTransactions)
}

// move the transaction into a new block
func (b *BlockChain) NewBlock(proof uint64) Block {

   prevHash := b.GetBlockHash(b.Chain[len(b.Chain)-1])

   block := Block{
      Index:        len(b.Chain),
      TimeStamp:    time.Now().Unix(),
      Transactions: b.CurrentTransactions,
      Pow:          proof,
      PrevHash:     prevHash,
   }

   b.Chain = append(b.Chain, block)

   b.CurrentTransactions = []Transaction{}

   return block
}

// verify a prrof of work
func (b BlockChain) VerifyPOW(lastProof uint64, curProof uint64) bool {
   lastProofStr := strconv.FormatUint(lastProof, 10)
   curProofStr := strconv.FormatUint(curProof, 10)
   hashValueTmp := sha256.Sum256([]byte(lastProofStr + curProofStr))
   hashValue := hex.EncodeToString(hashValueTmp[:])

   if hashValue[0:2] == "00" {
      return true
   } else {
      return false
   }
}

// get node identifier
func (b *BlockChain) GetNodeIdentifier() string {
   return b.NodeIdentifier
}

// get proof of work, find a number curProof that let make hash( chan[-1].pow + curProof)  begin with two zero
func (b *BlockChain) GetPOW() uint64 {
   lastProof := b.Chain[len(b.Chain)-1].Pow

   var curProof uint64 = 0

   for !b.VerifyPOW(lastProof, curProof) {
      curProof++
   }

   return curProof

}

BlcokChain 主要实现了以下几种方法:

  1. RegisterNode 注册节点方法,向当前的区块链节点注册周边的节点
  2. GetNodes, 获取周边节点的方法,在一致性检验时用得到
  3. GetBlockHash, 获取一整个区块的hash值, 在将区块链"串联"起来时用到
  4. VerifyChain, 区块链校验方法, 从递归区块开始依次向后遍历,根据前一个区块校验后一个区块的prevHash是否有效,根据前一个区块校验后一个区块的 pow 是否有效,如果遍历到最后一个节区块都是有效的,那么整个区块链就是有效的
  5. ResolveConflicts , 区块链一致性检测方法,当前节点回想周边节点请求周边节点的区块链,若周边节点区块链有效且更长,则以周边节点的区块链为准
  6. NewTransaction, 新增交易方法,只增加交易,并不打包
  7. GetCurrentTransactionSize, 返回带打包的交易量
  8. NewBlock, 打包方法, 对已有的交易进行打包
  9. VerifyPOW, 工作量证明校验方法, 查看上一个区块的工作量证明与当前工作量证明的 sha256 哈希结果是否以00打头。这里简单起见我只用了两个0,实际的区块链困难需要十几二十个0打头
  10. GetPOW, 工作量证明方法, 寻找一个随机数 使得 上一个区块的工作量证明与当前工作量证明的 sha256 哈希结果是否以00打头
  11. GetNodeIdentifier,获取当前区块链的唯一标识

main.go

package main

import (
   "block_chain_demo/block_chain"
   "encoding/json"
   "flag"
   "fmt"
   "io"
   "net/http"
)

var blockChain *block_chain.BlockChain

func mineHandler(w http.ResponseWriter, r *http.Request) {

   // check the transaction size, if it's empty return false
   if blockChain.GetCurrentTransactionsSize() == 0 {
      w.Write([]byte("current transaction empty"))
      return
   }

   // get proof of work
   proof := blockChain.GetPOW()

   // update transaction
   newTransaction := block_chain.Transaction{
      Sender:    "0",
      Recipient: blockChain.GetNodeIdentifier(),
      Amount:    1,
   }

   blockChain.NewTransaction(newTransaction)

   // new a block
   block := blockChain.NewBlock(proof)

   // return the new block
   resp, _ := json.Marshal(block)
   w.Write(resp)
   return

}

type FullChainResp struct {
   Chain        []block_chain.Block       `json:"chain"`
   Len          int                       `json:"len"`
   Transactions []block_chain.Transaction `json:"transactions"`
   Nodes        []string                  `json:"nodes"`
}

func fullChainHandler(w http.ResponseWriter, r *http.Request) {
   resp := &FullChainResp{
      Chain:        blockChain.Chain,
      Len:          len(blockChain.Chain),
      Transactions: blockChain.CurrentTransactions,
      Nodes:        blockChain.Nodes,
   }

   fmt.Println("full chain handler : ", blockChain.Chain)

   respBytes, _ := json.Marshal(resp)
   w.Write(respBytes)
   return
}

// new a transaction into block chain, need to be mined
func newTransactionHandler(w http.ResponseWriter, r *http.Request) {
   // parse body
   bodyBytes, err := io.ReadAll(r.Body)
   if err != nil {
      fmt.Println("Fail to read request body")
      w.Write([]byte("Fail to read request body"))
      return
   }

   var req block_chain.Transaction
   err = json.Unmarshal(bodyBytes, &req)
   if err != nil {
      fmt.Println("Fail to convert request body into transaction Req")
      fmt.Println("body = ", string(bodyBytes))
      w.Write([]byte("Fail to convert request body into transaction Req"))
      return
   }

   blockChain.NewTransaction(req)

   w.Write([]byte(fmt.Sprintf("the transaction will be added to index = %d block", len(blockChain.Chain))))
   return
}

type RegisterNodeReq struct {
   Node string `json:"node"`
}

// register a neighbour node into the block chain
func registerNodeHandler(w http.ResponseWriter, r *http.Request) {

   bodyBytes, err := io.ReadAll(r.Body)
   if err != nil {
      fmt.Println("Fail to read request body")
      w.Write([]byte("Fail to read request body"))
      return
   }

   var req RegisterNodeReq
   err = json.Unmarshal(bodyBytes, &req)
   if err != nil {
      fmt.Println("Fail to convert request body into transaction Req")
      fmt.Println("body = ", string(bodyBytes))
      w.Write([]byte("Fail to convert request body into transaction Req"))
      return
   }

   blockChain.RegisterNode(req.Node)
   nodes := blockChain.GetNodes()
   nodesStr := ""
   for i := 0; i < len(nodes); i++ {
      nodesStr = nodesStr + nodes[i] + " "
   }

   w.Write([]byte(nodesStr))
   return
}

// resolve conflicts with neighbour nodes
func resolveConflictsHandler(w http.ResponseWriter, r *http.Request) {
   blockChain.ResolveConflicts()
   chainBytes, _ := json.Marshal(blockChain.Chain)
   w.Write(chainBytes)
   return
}

func main() {

   var nodeIdentifier string
   var port string
   flag.StringVar(&nodeIdentifier, "ni", "", "node identifier")
   flag.StringVar(&port, "p", "", "http port")

   flag.Parse()
   fmt.Println("nodeIdentifier = ", nodeIdentifier)
   fmt.Println("port = ", port)

   blockChain = &block_chain.BlockChain{}
   blockChain.Init(nodeIdentifier)

   http.HandleFunc("/fullChain", fullChainHandler)
   http.HandleFunc("/newTransaction", newTransactionHandler)
   http.HandleFunc("/mine", mineHandler)
   http.HandleFunc("/registerNode", registerNodeHandler)
   http.HandleFunc("/resolveConflicts", resolveConflictsHandler)

   go func() {
      http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
   }()

   select {}

}

main.go 实现了主函数逻辑,以及区块链对外暴露的一些功能:

  1. fullChainHandler, 返回区块链的所有信息
  2. newTransactionHandler, 往区块链中加入校验信息,但并不打包
  3. mineHandler, 挖矿接口, 先进行工作量证明计算, 然后奖励自己一个虚拟币并加入 区块链交易中,最后调用 newBlcok 方法进行打包
  4. registerNodeHandler, 向当前节点注册相邻节点的接口
  5. resolveConflictsHandler, 一致性校验接口,调用 ResolveConflicts 方法进行一致性校验

至此,我们已经实现了一个简单的区块链,下面我们将这个区块链跑起来

测试区块链

运行区块链

首先我们需要将这个 go 项目进行编译, 在main目录下命令行输入

go build

这将在main目录下生成一个可执行文件 main 我们在当前main目录下命令行输入

./main  -ni node1 -p 9000

这将会启动一个 名叫 node1 的区块链,并且监听本地 9000端口

将可执行文件复制到另一个目录(随便一个目录,你喜欢就好),令行输入

./main  -ni node2 -p 10000

这将会启动一个 名叫 node2 的区块链,并且监听本地 10000端口

测试区块链

测试节点1 fullChain 接口

1712939567204.png 我们可以看到node1一开始只有一个初始块,这是符合预期的

测试节点2 fullChain 接口

1712939631275.png 我们可以看到node2一开始只有一个初始块,这也是符合预期的

测试节点1 newTransaction 接口

1712939706036.png 我们往 node1 区块链中新增了一条交易记录,返回这条交易将会被打入 index = 1 的区块中

1712939753378.png 我们调用一下fullChain 接口验证一下, 确实被加入了 tansactions 字段中

测试节点1 mine 接口

1712939805211.png 随后我们对node1 进行挖矿, 成功挖矿后返回打包好的区块

1712939861501.png 我们调用一下fullChain 接口验证一下, 之前的 transactions 被加入到了新的 block 中, 原有的 transactions 字段被清空了

测试节点2 注册接口

1712939945546.png 我们在 node2 中注册了一个节点 http://localhost:9000, 其实就是注册了 node1

1712939981294.png 我们调用一下fullChain 接口验证一下, 确实新增了一个邻近节点

测试节点2 一致性校验接口

1712940067233.png 我们调用 resolveConflict 进行一致性校验, 可以看到返回了的 chain 里面已经由两个 block了,这个 chain 和 node1 中的 chain 一模一样

1712940127584.png 我们调用一下fullChain 接口验证一下, chain 确实被更新了,和 node1 中的 chain 保持一致

至此, 我们已经是心啊了一个极简的区块链并完成了测试

巨人的肩膀

hackernoon.com/learn-block…