乐趣区

Go语言实现一个区块链

本文将逐步拆解实现区块链功能的几个步骤

你需要掌握的基本知识:

  • 什么是区块链
  • sha256 哈希加密算法
  • go 语言基础,包括 goroutine 和 channel 的理解

准备工作

  • go get github.com/davecgh/go-spew/spew spew 是一个非常好的打印输出工具,可以在终端输出 struct 和 slice 数据
  • go get github.com/gorilla/mux mux 可以用来处理 http 请求,帮助我们快速搭建一个 go 服务器
  • go get github.com/joho/godotenv 这个包可以读取.env 文件中的变量

.env 文件需要在项目的根目录下,一般在 main.go 所在位置

  • 一款给力的 IDE,比如 Goland

几个概念

  • 挖矿,挖矿其实就是通过解决一类数学难题,得到在现有区块链上创建一个区块的权利,并获得一些奖励,比如比特币,以太币等。
  • PoW(Proof of work),简单来说 PoW 就是:有一个 Nonce 值(值随意),这个 Nonce 值和区块的数据结合在一起通过 SHA256 得到一个哈希值,如果这个哈希值的前 N(difficulty) 位字符都是 0,那么就算解决了这个数学难题,可以创建一个区块。
  • 区块链,区块链是前后紧密连接的,每一个 Block 都会记录上一个区块的哈希,如果当前生成的区块的所记录的 PrevHash 与上一区块不同的话,那么此次生成就无效,同理,如果任何一个人想在区块链的某一个区块上修改数据,那么就会造成整个链无效。

创建项目

  • 在 $GOPATH 的 src 下创建项目 blockChain
  • 在 blockChain 下创建文件.env 和 main.go

在.env 文件中写入 PORT=8088

需要引入的包

import (
   "crypto/sha256"
   "encoding/hex"
   "time"
   "os"
   "log"
   "net/http"
   "github.com/gorilla/mux"
   "encoding/json"
   "io"
   "github.com/davecgh/go-spew/spew"
   "sync"
   "strconv"
   "strings"
   "fmt"
   "github.com/joho/godotenv"
   "net"
   "bufio"
)

区块逻辑

定义 Block 区块结构体

type Block struct {
   Index int // 表示区块所在区块链的位置
   Timestamp string // 生成区块的时间戳
   Data int // 写入区块的数据
   Hash string // 整个区块数据 SHA256 的哈希
   PrevHash string // 上一个区块的哈希值
   Difficulty int // 定义难度
   Nonce string // 定义一个 Nonce
}

定义常量和一些变量

const difficulty = 1 // 定义难度,也就是哈希包含多少个 0 的前缀
var mutex = &sync.Mutex{} // 防止并发写入请求造成的错误,加入互斥锁
var BlockChain []Block // 定义一个区块链,数据元素要全部都是 Block
var bcServer chan []Block // 定义一个 channel,处理各个节点之间的同步问题 

计算区块哈希

/**
计算区块哈希值
 */
func calculateHash(block Block) string {record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.Data) + block.PrevHash + block.Nonce // 得到当前 block 区块的字符串拼接,按照索引、时间戳、所含数据、上一个区块哈希来进行记录,Nonce 值一并加入
   h := sha256.New() // 得到 sha256 哈希算法
   h.Write([]byte(record)) // 得到对应哈希
   hashed := h.Sum(nil)
   return hex.EncodeToString(hashed) // 转化为字符串返回
}

生成新的区块

/**
生成一个区块,根据上一个区块
 */
func generateBlock(oldBlock Block, Data int) (Block, error) {
   var newBlock Block
   t := time.Now()
   newBlock.Index = oldBlock.Index + 1 // 索引自增
   newBlock.Timestamp = t.String() // 时间戳
   newBlock.Data = Data // 数据
   newBlock.PrevHash = oldBlock.Hash // 上一个区块的哈希
   newBlock.Difficulty = difficulty // 难度
   //newBlock.Hash = calculateHash(newBlock) // 计算本区块的哈希
   for i := 0; ; i++ {hex := fmt.Sprintf("%x", i) // 16 进制展示
      newBlock.Nonce = hex
      newHash := calculateHash(newBlock) // 计算哈希
      if !isHashValid(newHash, newBlock.Difficulty) {fmt.Println(newHash, "继续努力!????")
         time.Sleep(time.Second) // 每隔 1s 执行一次
         continue
      } else {fmt.Println(newHash, "已经成功!")
         newBlock.Hash = newHash
         break
      }
   }
   return newBlock, nil
}

验证区块是否合法

/**
验证区块是否合法
 */
func isBlockValid(newBlock, oldBlock Block) bool {
   if oldBlock.Index + 1 != newBlock.Index { // 如果索引不继承自上一个,验证不通过
      return false
   }
   if oldBlock.Hash != newBlock.PrevHash { // 如果哈希不继承上一个区块,验证不通过
      return false
   }
   if calculateHash(newBlock) != newBlock.Hash { // 如果计算出来的哈希不一致,验证不通过
      return false
   }
   return true
}

验证哈希是否符合 PoW

/**
验证哈希的前缀是否包含 difficulty 个 0
 */
func isHashValid(hash string, difficulty int) bool {prefix := strings.Repeat("0", difficulty)
   return strings.HasPrefix(hash, prefix)
}

选择长链

因为在实际场景中,区块链可能会产生分叉,造成 A 和 B 长短不一的情况,故而选择长的作为新链

/**
选择长链作为正确的链
 */
 func replaceChain(newBlocks []Block) {if len(newBlocks) > len(BlockChain) { // 计算数组长度
      BlockChain = newBlocks
   }
 }

同步节点逻辑


如图所示,节点数据同步就是通过新节点中生成一个区块后,先通过 channel 传递给主线程,然后主线程广播给各个节点来完成的。

监听连接逻辑

/**
处理连接
 */
 func handleConn(conn net.Conn) {defer conn.Close() // 完成后关闭
   spew.Dump(conn)
   io.WriteString(conn, "输入数字:")
   scanner := bufio.NewScanner(conn)

   go func() {for scanner.Scan() { // 轮询扫描所有 tcp 连接
         data, err := strconv.Atoi(scanner.Text())

         if err != nil {log.Printf("%v 非数字", scanner.Text(), err)
         }
         newBlock, err := generateBlock(BlockChain[len(BlockChain) - 1], data)

         if err != nil {log.Println(err)
            continue
         }

         if isBlockValid(newBlock, BlockChain[len(BlockChain) - 1]) {newBlockChain := append(BlockChain, newBlock)
            replaceChain(newBlockChain)
         }

         bcServer <- BlockChain // 将生成的区块数据交给通道,单向传递
         io.WriteString(conn, "\n 输入数字:")
      }
   }()

   go func() {
      for { // 每隔 10s 同步一次
         time.Sleep(10 * time.Second)
         output, err := json.MarshalIndent(BlockChain, ""," ")

         if err != nil {log.Fatal(err)
         }

         io.WriteString(conn, "\n↓↓↓↓↓↓↓↓↓↓↓↓↓ 同步区块链:↓↓↓↓↓↓↓↓↓↓↓↓↓↓\n"+ string(output) + "\n↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑\n")
      }
   }()

   for _= range bcServer {spew.Dump(BlockChain)
   }
 }

主函数

func main () {err := godotenv.Load()
   if err != nil {log.Fatal(err)
}

bcServer = make(chan []Block) // 创建通道

t := time.Now()
genesisBlock := Block{0, t.String(), 0, "","", difficulty, ""}
spew.Dump(genesisBlock)
BlockChain = append(BlockChain, genesisBlock) // 创世区块

server, err := net.Listen("tcp", ":" + os.Getenv("PORT")) // 监听 TCP 端口
if err != nil {log.Fatal(err)
}
defer server.Close() // 完成后关闭 server

for {conn, err := server.Accept()
   if err != nil {log.Fatal()
   }
   go handleConn(conn) // 协程处理连接
}
}

全量代码

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "time"
    "os"
    "log"
    "net/http"
    "github.com/gorilla/mux"
    "encoding/json"
    "io"
    "github.com/davecgh/go-spew/spew"
    "sync"
    "strconv"
    "strings"
    "fmt"
    "github.com/joho/godotenv"
    "net"
    "bufio"
)

//////////////////// 处理区块链 ////////////////////
const difficulty = 1 // 定义难度,也就是哈希包含多少个 0 的前缀
type Block struct {
    Index int // 表示区块所在区块链的位置
    Timestamp string // 生成区块的时间戳
    Data int // 写入区块的数据
    Hash string // 整个区块数据 SHA256 的哈希
    PrevHash string // 上一个区块的哈希值
    Difficulty int // 定义难度
    Nonce string // 定义一个 Nonce
}
var mutex = &sync.Mutex{} // 防止并发写入请求造成的错误,加入互斥锁
var BlockChain []Block // 定义一个区块链,数据元素要全部都是 Block
var bcServer chan []Block // 定义一个 channel,处理各个节点之间的同步问题
/**
计算区块哈希值
 */
func calculateHash(block Block) string {record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.Data) + block.PrevHash + block.Nonce // 得到当前 block 区块的字符串拼接,按照索引、时间戳、所含数据、上一个区块哈希来进行记录,Nonce 值一并加入
    h := sha256.New() // 得到 sha256 哈希算法
    h.Write([]byte(record)) // 得到对应哈希
    hashed := h.Sum(nil)
    return hex.EncodeToString(hashed) // 转化为字符串返回
}
/**
生成一个区块,根据上一个区块
 */
func generateBlock(oldBlock Block, Data int) (Block, error) {
    var newBlock Block
    t := time.Now()
    newBlock.Index = oldBlock.Index + 1 // 索引自增
    newBlock.Timestamp = t.String() // 时间戳
    newBlock.Data = Data // 数据
    newBlock.PrevHash = oldBlock.Hash // 上一个区块的哈希
    newBlock.Difficulty = difficulty // 难度
    //newBlock.Hash = calculateHash(newBlock) // 计算本区块的哈希
    for i := 0; ; i++ {hex := fmt.Sprintf("%x", i) // 16 进制展示
        newBlock.Nonce = hex
        newHash := calculateHash(newBlock) // 计算哈希
        if !isHashValid(newHash, newBlock.Difficulty) {fmt.Println(newHash, "继续努力!????")
            time.Sleep(time.Second) // 每隔 1s 执行一次
            continue
        } else {fmt.Println(newHash, "已经成功!")
            newBlock.Hash = newHash
            break
        }
    }
    return newBlock, nil
}

/**
验证区块是否合法
 */
func isBlockValid(newBlock, oldBlock Block) bool {
    if oldBlock.Index + 1 != newBlock.Index { // 如果索引不继承自上一个,验证不通过
        return false
    }
    if oldBlock.Hash != newBlock.PrevHash { // 如果哈希不继承上一个区块,验证不通过
        return false
    }
    if calculateHash(newBlock) != newBlock.Hash { // 如果计算出来的哈希不一致,验证不通过
        return false
    }
    return true
}
/**
验证哈希的前缀是否包含 difficulty 个 0
 */
func isHashValid(hash string, difficulty int) bool {prefix := strings.Repeat("0", difficulty)
    return strings.HasPrefix(hash, prefix)
}

/**
选择长链作为正确的链
 */
 func replaceChain(newBlocks []Block) {if len(newBlocks) > len(BlockChain) { // 计算数组长度
         BlockChain = newBlocks
    }
 }

 ////////////////// 主函数 /////////////////
 
 func main () {err := godotenv.Load()
     if err != nil {log.Fatal(err)
    }

    bcServer = make(chan []Block) // 创建通道

    t := time.Now()
    genesisBlock := Block{0, t.String(), 0, "","", difficulty, ""}
    spew.Dump(genesisBlock)
    BlockChain = append(BlockChain, genesisBlock) // 创世区块

    server, err := net.Listen("tcp", ":" + os.Getenv("PORT")) // 监听 TCP 端口
    if err != nil {log.Fatal(err)
    }
    defer server.Close() // 完成后关闭 server

    for {conn, err := server.Accept()
        if err != nil {log.Fatal()
        }
        go handleConn(conn) // 协程处理连接
    }
 }
/**
处理连接
 */
 func handleConn(conn net.Conn) {defer conn.Close() // 完成后关闭
    spew.Dump(conn)
     io.WriteString(conn, "输入数字:")
     scanner := bufio.NewScanner(conn)

     go func() {for scanner.Scan() { // 轮询扫描所有 tcp 连接
             data, err := strconv.Atoi(scanner.Text())

             if err != nil {log.Printf("%v 非数字", scanner.Text(), err)
            }
            newBlock, err := generateBlock(BlockChain[len(BlockChain) - 1], data)

            if err != nil {log.Println(err)
                continue
            }

            if isBlockValid(newBlock, BlockChain[len(BlockChain) - 1]) {newBlockChain := append(BlockChain, newBlock)
                replaceChain(newBlockChain)
            }

            bcServer <- BlockChain // 将生成的区块数据交给通道,单向传递
            io.WriteString(conn, "\n 输入数字:")
        }
    }()

     go func() {
         for { // 每隔 10s 同步一次
             time.Sleep(10 * time.Second)
             output, err := json.MarshalIndent(BlockChain, ""," ")

             if err != nil {log.Fatal(err)
            }

            io.WriteString(conn, "\n↓↓↓↓↓↓↓↓↓↓↓↓↓ 同步区块链:↓↓↓↓↓↓↓↓↓↓↓↓↓↓\n"+ string(output) + "\n↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑\n")
        }
    }()

     for _= range bcServer {spew.Dump(BlockChain)
    }
 }

运行

  • 打开终端,运行 go run main.go,作为主线程终端
  • 新开两个终端作为节点,运行 nc localhost 8088 或 telnet localhost 8088,输入相应的数字
  • 等待生成区块,主线程显示如下

  • 各个节点每过 10s 会接收主线程的同步区块链数据

  • 你可以更换 difficulty 常量的值为 2 或 3,计算时间会成倍增加。

以上节点间的广播同步是通过 tcp 连接来实现的,但更好的方案应该是 p2p 网络,需要安装 libp2p 包,这里不做赘述。

退出移动版