4

200行GO代码实现区块链3

 2 years ago
source link: https://www.hi-roy.com/posts/200%E8%A1%8Cgo%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0%E5%8C%BA%E5%9D%97%E9%93%BE3/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

200行GO代码实现区块链3

2018-06-22

原文,阅读之前请先看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进行测试以及测试输出的部分就不翻译了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK