200行GO代码实现区块链3

原文,阅读之前请先看200行GO代码实现区块链1200行GO代码实现区块链2

如果看到这了相信你已经知道什么是加密算法等背景了,所以忽略关于这部分的翻译,直接从编码开始。这篇文章在前两篇的文章基础上添加了工作量证明(POW)挖矿算法。

首先创建.env文件来定义环境变量,里面只有一行ADDR=8080,然后创建main.go并引入相关依赖:

package main

import (
        "crypto/sha256"
        "encoding/hex"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "os"
        "strconv"
        "strings"
        "sync"
        "time"

        "github.com/davecgh/go-spew/spew"
        "github.com/gorilla/mux"
        "github.com/joho/godotenv"
)

如果你阅读过之前的文章,你应该知道区块链中的区块通过比较本区块记录的PrevHash和前一个区块的Hash来进行验证,这也是保证区块链完整性和坏人无法改变区块链历史的原因。

BMP代表心跳速率,我们使用这个作为存储在区块中的数据,接下来定义数据模型和需要的变量:

const difficulty = 1

type Block struct {
        Index      int
        Timestamp  string
        BPM        int
        Hash       string
        PrevHash   string
        Difficulty int
        Nonce      string
}

var Blockchain []Block

type Message struct {
        BPM int
}

var mutex = &sync.Mutex{}

difficulty定义了难度,即hash值开头0的数量。0数量越多,则难度越大,这里我们要求开头有1个0。

Block是区块的数据结构,别忘了Nonce,我们晚一些解释这个。

BlockchainBlock组成的列表(Roy注:准确的说是slice,不过翻译成切片有点拗口),用来存储区块链。

Message用来接收我们向REST API使用POST方式生成新区块的数据。

我们声明了mutex来数据冲突并且确保区块不会同一时刻生成多个。

接下来创建web服务,首先创建run()函数晚些将在main函数中调用,同时生成了makeMuxRouter()来管理路由。记住,我们使用GET来检索区块POST来添加新区块,由于区块链是不可变的所以我们不需要删除或编辑功能。

func run() error {
        mux := makeMuxRouter()
        httpAddr := os.Getenv("ADDR")
        log.Println("Listening on ", os.Getenv("ADDR"))
        s := &http.Server{
                Addr:           ":" + httpAddr,
                Handler:        mux,
                ReadTimeout:    10 * time.Second,
                WriteTimeout:   10 * time.Second,
                MaxHeaderBytes: 1 << 20,
        }

        if err := s.ListenAndServe(); err != nil {
                return err
        }

        return nil
}

func makeMuxRouter() http.Handler {
        muxRouter := mux.NewRouter()
        muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
        muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
        return muxRouter
}

httpAddr := os.Getenv("ADDR")这行代码从.env文件中读取我们定义的:8080,这样就可以通过浏览器访问http://localhost:8080来查看应用了。(Roy注:注意这里的1 << 20这个位移操作,正好是1KB。)

现在编写处理GET请求的函数来在浏览器展示我们的区块链,同时添加respondwithJSON函数来打印错误信息:


func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {
        bytes, err := json.MarshalIndent(Blockchain, "", "  ")
        if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
        }
        io.WriteString(w, string(bytes))
}

func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {
        w.Header().Set("Content-Type", "application/json")
        response, err := json.MarshalIndent(payload, "", "  ")
        if err != nil {
                w.WriteHeader(http.StatusInternalServerError)
                w.Write([]byte("HTTP 500: Internal Server Error"))
                return
        }
        w.WriteHeader(code)
        w.Write(response)
}

如果你觉得一头雾水,请先看之前的文章。

接下来编写处理生成区块的POST请求函数,我们通过发送JSON类型的数据比如{"BMP":60}http://localhost:8080来生成新区块:

func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        var m Message

        decoder := json.NewDecoder(r.Body)
        if err := decoder.Decode(&m); err != nil {
                respondWithJSON(w, r, http.StatusBadRequest, r.Body)
                return
        }   
        defer r.Body.Close()

        //ensure atomicity when creating new block
        mutex.Lock()
        newBlock := generateBlock(Blockchain[len(Blockchain)-1], m.BPM)
        mutex.Unlock()

        if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
                Blockchain = append(Blockchain, newBlock)
                spew.Dump(Blockchain)
        }   

        respondWithJSON(w, r, http.StatusCreated, newBlock)

}

注意mutex加锁和解锁的地方,我们在写新区块之前加锁,否则将可能造成数据冲突。有些读者可能注意到了generateBlock函数,这是实现工作量证明的关键函数,我们一会再说。

首先添加isBlockValid函数来确保区块链的索引递增并且每个区块的PrevHash和前一个区块的Hash相匹配。

然后添加calculateHash函数来计算创建Hash值,这里我们使用SHA256来链接Index、Timestamp,BMP,PrevHash和Nonce(我们晚一点解释这个)。

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
}

func calculateHash(block Block) string {
        record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.BPM) + block.PrevHash + block.Nonce
        h := sha256.New()
        h.Write([]byte(record))
        hashed := h.Sum(nil)
        return hex.EncodeToString(hashed)
}

接下来编写挖矿算法——工作量证明(POW),我们要确保在新区块添加到区块链之前工作量证明已经完成,让我们先写一个简单的函数来检查生成的散列是否满足条件:

  • 生成的散列是否以0开头
  • 开头0的数量是否和我们常量difficulty中定义的一致(本例中为1)
  • 我们可以通过增大难度来使挖矿变难

函数isHashValid如下:

func isHashValid(hash string, difficulty int) bool {
        prefix := strings.Repeat("0", difficulty)
        return strings.HasPrefix(hash, prefix)
}

GO在strings包中提供了RepeatHasPrefix函数,变量prefix是重复了difficulty次的0组成的字符串,接下来我们判断散列是否以这个字符串开头,如果是返回True否则返回False

接下来构建generateBlock函数:

func generateBlock(oldBlock Block, BPM int) Block {
        var newBlock Block

        t := time.Now()

        newBlock.Index = oldBlock.Index + 1
        newBlock.Timestamp = t.String()
        newBlock.BPM = BPM
        newBlock.PrevHash = oldBlock.Hash
        newBlock.Difficulty = difficulty

        for i := 0; ; i++ {
                hex := fmt.Sprintf("%x", i)
                newBlock.Nonce = hex
                if !isHashValid(calculateHash(newBlock), newBlock.Difficulty) {
                        fmt.Println(calculateHash(newBlock), " do more work!")
                        time.Sleep(time.Second)
                        continue
                } else {
                        fmt.Println(calculateHash(newBlock), " work done!")
                        newBlock.Hash = calculateHash(newBlock)
                        break
                }

        }
        return newBlock
}

这里创建新区块并将前一个区块的Hash存储到本区块的PrevHash中来确保连续性,其他字段也很明显:

  • Index自增
  • Timestamp记录当前时间
  • BMP记录心跳数据
  • Difficulty简单的记录了程序最上面定义的常量。本文中不会使用,但未来如果我们需要确保难度和当前散列结果一致(比如散列前面有N位0,这个值应该和Difficulty相等)时将要用到。

for循环在这里是很关键的一步,我们来看看这里都做了些什么:

  • 首先我们将16进制的i值赋值给了Nonce,我们的calculateHash函数需要这个变量来进行Hash计算,如果计算结果0的个数不满足要求,我们则尝试一个新值。
  • 我们从0开始循环,并判断其结果0开头的个数是否和difficulty规定的一样,如果不同则进行下一次循环。
  • 我们添加了sleep1秒钟来模拟工作量证明算法中某些耗时操作。
  • 进行循环直到获得一个开头0的个数满足我们需求的数值,也就意味着工作量证明算法成功执行。此时才准许新区块通过handleWriteBlock添加到区块链中。

所有需要的函数都完成了,现在编写main函数:


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

        go func() {
                t := time.Now()
                genesisBlock := Block{}
                genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), "", difficulty, ""}
                spew.Dump(genesisBlock)

                mutex.Lock()
                Blockchain = append(Blockchain, genesisBlock)
                mutex.Unlock()
        }()
        log.Fatal(run())

}

通过调用godotenv.Load()来载入环境变量,这里是:8080端口。然后创建一个go routine 创建了创世块作为整个区块链的起始,最后调用run()函数来运行web服务。

完整代码在这里


核心部分就翻译到这,原文还有一些如何使用postman进行测试以及测试输出的部分就不翻译了。