Github上有一个Repo,是一个使用Go语言(golang),不到200行代码写的区块链源代码,准确的说是174行。原作者起了个名字是 Code your own blockchain in less than 200 lines of Go! 而且作者也为此写了一篇文章。https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc
这篇文章是一个大概的思路和代码的实现,当然还有很多代码的逻辑没有涉及,所以我就针对这不到200行的代码进行一个分析,包含原文章里没有涉及到的知识点,对Go语言,区块链都会有一个更深的认识。
所有的源代码都在这里:https://github.com/nosequeldeebee/blockchain-tutorial/blob/master/main.go
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"strconv"
"sync"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
在源代码的开头,是作者引入的一些包,有标准的,也有第三方的。像sha256,hex这些标准包是为了sha-256编码用的,其他还有启动http服务,打印日志的log,并发控制的sync,时间戳的time。
第三方包有三个,其中两个我都详细介绍过,相信大家不会陌生。
go-spew
是一个变量结构体的调试利器,可以打印出变量结构体对应的数据和结构,调试非常方便
gorilla/mux
是一个web路由服务,可以很简单的帮我们构建web服务。
不过目前用gin的比较多,也推荐使用gin https://github.com/gin-gonic/gin。
godotenv
是一个读取配置文章的库,可以让我们读取.env
格式的配置文件,比如从配置文件里读取IP、PORT等。不过目前配置文件还是推荐YAML和TOML,对应的第三方库是:
gopkg.in/yaml.v21https://github.com/BurntSushi/toml
既然要写一个区块链,那么肯定的有一个区块的实体,我们通过golang的struct
来实现。
// Block represents each 'item' in the blockchain
type Block struct {
Index int
Timestamp string
BPM int
Hash string
PrevHash string
}
Block
里包含几个字段:
Index 就是Block的顺序索引
Timestamp是生成Block的时间戳
BPM,作者说代表心率,每分钟心跳数
Hash是通过sha256生成的散列值,对整个Block数据的Hash
PrevHash 上一个Block的Hash,这样区块才能连在一起构成区块链
有了区块Block了,那么区块链就非常好办了。
// Blockchain is a series of validated Blocks
var Blockchain []Block
就是这么简单,一个Block数组就是一个区块链。区块链的构成关键在于Hash和PrevHash,通过他们一个个串联起来,就是一串Block,也就是区块链。因为相互之间通过Hash和PrevHash进行关联,所以整个链很难被篡改,链越长被篡改的成本越大,因为要把整个链全部改掉才能完成篡改的目的,这样的话,其他节点验证这次篡改肯定是不能通过的。
既然关键点在于Hash,所以我们要先算出来一个Block的数据的Hash,也就是对Block里的字段进行Hash,计算出一个唯一的Hash值。
// SHA256 hasing
func calculateHash(block Block) string {
record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.BPM) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
sha256
是golang内置的sha256的散列标准库,可以让我们很容易的生成对应数据的散列值。从源代码看,是把Block
的所有字段进行字符串拼接,然后通过sha256
进行散列,散列的数据再通过hex.EncodeToString
转换为16进制的字符串,这样就得到了我们常见的sha256
散列值,类似这样的字符串8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
。
Block的散列值被我们计算出来了,Block的Hash和PrevHash这两个字段搞定了,那么我们现在就可以生成一个区块了,因为其他几个字段都是可以自动生成的。
// create a new block using previous block's hash
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.Hash = calculateHash(newBlock)
return newBlock
}
因为区块链是顺序相连的,所以我们在生成一个新的区块的时候,必须知道上一个区块,也就是源代码里的oldBlock
。另外一个参数BPM
就是我们需要在区块里存储的数据信息了,这里作者演示的例子是心率,我们可以换成其他业务中想要的数据。
Index
是上一个区块的Index+1
,保持顺序;Timestamp
通过time.Now()
可以得到;Hash通过calculateHash
方法计算出来。这样我们就生成了一个新的区块。
在这里作者并没有使用POW(工作量证明)这类算法来生成区块,而是没有任何条件的,这里主要是为了模拟区块的生成,演示方便。
区块可以生成了,但是生成的区块是否可信,我们还得对他进行校验,不能随便生成一个区块。在比特币(BitCoin)中校验比较复杂,这里作者采用了简单模拟的方式。
// make sure block is valid by checking index, and comparing the hash of the previous block
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
}
简单的对比Index
,Hash
是否是正确的,并且重新计算了一遍Hash
,防止被篡改。
到了这里,关于区块链的代码已经全部完成了,剩下的就是把区块链的生成、查看等包装成一个Web服务,可以通过API、浏览器访问查看。因为作者这里没有实现P2P网络,所以采用的是WEB服务的方式。
// create handlers
func makeMuxRouter() http.Handler {
muxRouter := mux.NewRouter()
muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
return muxRouter
}
通过mux
定义了两个Handler,URL都是/
,但是对应的Method
是不一样的。
GET
方法通过handleGetBlockchain
函数实现,用于获取区块链的信息。
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))
}
Blockchain
是一个[]Block
,handleGetBlockchain
函数的作用是把Blockchain
格式化为JSON字符串,然后显示出来。io.WriteString
是一个很好用的函数,可以往Writer
里写入字符串。更多参考 Go语言实战笔记(十九)| Go Writer 和 Reader
'POST'方法通过handleWriteBlock
函数实现,用于模拟区块的生成。
func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
//使用了一个Mesage结构体,更方便的存储BPM
var msg Message
//接收请求的数据信息,类似{"BPM":60}这样的格式
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&msg); err != nil {
respondWithJSON(w, r, http.StatusBadRequest, r.Body)
return
}
defer r.Body.Close()
//控制并发,生成区块链,并且校验
mutex.Lock()
prevBlock := Blockchain[len(Blockchain)-1]
newBlock := generateBlock(prevBlock, msg.BPM)
//校验区块链
if isBlockValid(newBlock, prevBlock) {
Blockchain = append(Blockchain, newBlock)
spew.Dump(Blockchain)
}
mutex.Unlock()
//返回新的区块信息
respondWithJSON(w, r, http.StatusCreated, newBlock)
}
以上代码我进行了注释,便于理解。主要是通过POST发送一个{"BPM":60}
格式的BODY来添加区块,如果格式正确,那么就生成区块进行校验,合格了就加入到区块里;如果格式不对,那么返回错误信息。
用于控制并发的锁可以参考Go语言实战笔记(十七)| Go 读写锁
这个方法里有个Message
结构体,主要是为了便于操作方便。
// Message takes incoming JSON payload for writing heart rate
type Message struct {
BPM int
}
返回的JSON信息,也被抽取成了一个函数respondWithJSON
,便于公用。
func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {
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)
}
好了,快完成了,以上Web的Handler已经好了,现在我们要启动我们的Web服务了。
// web server
func run() error {
mux := makeMuxRouter()
//从配置文件里读取监听的端口
httpPort := os.Getenv("PORT")
log.Println("HTTP Server Listening on port :", httpPort)
s := &http.Server{
Addr: ":" + httpPort,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 <20,
}
if err := s.ListenAndServe(); err != nil {
return err
}
return nil
}
和原生的http.Server
基本一样,应该比较好理解。mux其实也是一个Handler,这就是整个Handler处理链。现在我们就差一个main
主函数来启动我们整个程序了。
//控制并发的锁
var mutex = &sync.Mutex{}
func main() {
//加载env配置文件
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
//开启一个goroutine生成一个创世区块
go func() {
t := time.Now()
genesisBlock := Block{}
genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), ""}
spew.Dump(genesisBlock)
mutex.Lock()
Blockchain = append(Blockchain, genesisBlock)
mutex.Unlock()
}()
log.Fatal(run())
}
整个main
函数并不太复杂,主要就是加载env配置文件,开启一个go协程生成一个创世区块并且添加到区块链的第一个位置,然后就是通过run
函数启动Web服务。
一个区块链都有一个创世区块,也就是第一个区块。有了第一个区块我们才能添加第二个,第三个,第N个区块。创世区块因为是第一个区块,所以它是没有PrevHash
的。
终于可以运行了,假设我们设置的PORT是8080,现在我们通过go run main.go
启动这个简易的区块链程序,就可以看到控制台输出的创世区块信息。然后我们通过浏览器打开http://localhost:8080
也可以看到这个区块链的信息,里面只有一个创世区块。
如果我们要新增一个区块,通过curl或者postman,向http://localhost:8080
发送body格式为{"BPM":60}
的POST的信息即可。然后在通过浏览器访问http://localhost:8080
查看区块链信息,验证是否已经添加成功。
到这里,整个源代码的分析已经完了,我们看下这个简易的区块链涉及到多少知识:
sha256散列
字节到16进制转换
并发同步锁
Web服务
配置文件
后向式链表
结构体
JSON
……
等等,上面的很多知识,我已经在文章中讲解或者通过以前些的文章说明,大家可以看一下,详细了解。
推荐阅读
go语言的开源区块链代码都有哪些?欢迎留言补充
站长 polarisxu
自己的原创文章
不限于 Go 技术
职场和创业经验
Go语言中文网
每天为你
分享 Go 知识
Go爱好者值得关注