Go语言实现一个区块链

3

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

你需要掌握的基本知识:

  • 什么是区块链
  • 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,输入相应的数字
  • 等待生成区块,主线程显示如下

clipboard.png

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

clipboard.png

  • 你可以更换difficulty常量的值为2或3,计算时间会成倍增加。
以上节点间的广播同步是通过tcp连接来实现的,但更好的方案应该是p2p网络,需要安装libp2p包,这里不做赘述。

如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

载入中...