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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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代表心跳速率,我们使用这个作为存储在区块中的数据,接下来定义数据模型和需要的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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来添加新区块,由于区块链是不可变的所以我们不需要删除或编辑功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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函数来打印错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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来生成新区块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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(我们晚一点解释这个)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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如下:

1
2
3
4
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函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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进行测试以及测试输出的部分就不翻译了。