200行GO代码实现区块链3
原文,阅读之前请先看200行GO代码实现区块链1 和 200行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
,我们晚一些解释这个。
Blockchain
是Block
组成的列表(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
包中提供了Repeat
和HasPrefix
函数,变量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
规定的一样,如果不同则进行下一次循环。 - 我们添加了
sleep
1秒钟来模拟工作量证明算法中某些耗时操作。 - 进行循环直到获得一个开头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
进行测试以及测试输出的部分就不翻译了。