200行GO代码实现区块链1
原文,需要科学上网。省略了一些无关的内容。
世界上很多开发者听说过区块链却不知道它是怎么工作的,他们或许仅仅听过比特币或者智能合约一类的名词。这篇文章尝试用简明的语言说明区块链并且用不到200行的代码来实现你自己的区块链!文章的最后,你应该可以运行并且添加区块到区块链中并从浏览器中看到结果。
有什么学习区块链的方法比你亲自实现一个更好呢?
文章讲了什么
- 如何创建自己的区块链
- 如何使用Hash来维护区块链的完整性
- 如何添加新区块
- 如何解决多个节点同时生成区块导致冲突问题
- 如何在浏览器中查看区块链
- 如何生成新区块
- 了解区块链的基础知识后,你可以决定你的未来发展方向
文章没讲什么
为了保持简单,我们并没介绍更高级的概念比如pow和pos的对比,我们模拟了网络交互所以你可以看到区块链并且添加区块,但网络广播部分将以后再讲。
准备工作
因为我们使用GO,所以假设你已经是一个有经验的GO开发者了。安装并设置GO开发环境后,需要安装下面的三方包:
go get github.com/davecgh/go-spew/spew
spew
可以更好的输出struct
和slices
,你值得拥有。
go get github.com/gorilla/mux
mux
是一个流行的web服务框架,我们需要这个。
go get github.com/joho/godotenv
godotenv
让我们从根目录的.env
文件读取配置信息,这样http端口一类的配置就不需要硬编码在代码中了。
让我们在根目录创建一个.env
文件,里面定义我们http服务的端口,内容就一行:
ADDR=8080
再创建一个main.go
文件,所有的代码都将写在这里并且不会超过200行,让我们开始吧!
imports
首先引入我们需要的库:
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
数据模型
接下来定义一个struct作为区块,别担心,我们下面将解释每一个字段的作用:
type Block struct {
Index int
Timestamp string
BPM int
Hash string
PrevHash string
}
每一个Block中的数据都会写入到区块链中:
Index
表示区块在区块链中的位置Timestamp
是自动生成的并记录了数据写入时间BPM
表示每分钟心跳次数Hash
是sha256加密后的数据PrevHash
表示前一个区块的Hash值
接下来,定义一个由Block组成的slices:
var Blockchain []Block
那么是如何把区块组成区块链的呢?我们使用Hash值来识别和保证区块的顺序正确。确保每个块中的PrevHash
和前一个区块中的Hash
一样,这样我们就知道了区块链的顺序。
Hashing和创建新区块
为什么需要进行Hash?主要有2个原因:
- 节省空间。Hash结果是由区块中全部数据计算产生的,在我们这个例子中仅仅有少量数据,但想象一下如果一个区块中有成百上千的数据,记录数据hash后的结果显然比一次又一次的拷贝全部数据更加高效。
- 保持区块链完整性。如上图,通过记录上一个区块的Hash值,我们能够确保区块链的顺序是正确的。如果有人恶意写入数据(比如想修改心跳来影响保险价格),hash值将被改变而且区块链将被"打破”,并且每个人都能知道并且不信任那个恶意的链条。
现在创建一个函数来计算区块的Hash值:
func calculateHash(block Block) string {
record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
calculateHash
函数将Index
、Timestamp
、BPM
、PrevHash
链接成一个字符串并返回其SHA256后的结果。现在我们可以使用这些参数通过generateBlock
函数创建一个新区块了。我们需要传入前一个区块和BPM值,不用担心这个BPM我们晚一些解释:
func generateBlock(oldBlock Block, BPM int) (Block, error) {
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, nil
}
注意时间是使用time.now()
自动创建的并且我们调用了之前写的calculateHash
函数,PrevHash直接从前一个区块中拷贝出来,Index则是根据前一个区块的值自增1。
验证区块
现在我们需要写一些函数来确保区块链的真实性,通过检测Index来确保正确的自增,还需要检测PrevHash和前一个区块的Hash是否相同。最后我们再次使用calculateHash
函数检查当前区块的Hash值。现在写一个isBlockValid
函数并且返回bool
类型,如果通过了全部的检查则返回true
:
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
}
如果有2个节点同时计算出正确的Hash值并且添加到各自的区块链中,那么我们该相信哪一条呢?我们选择最长的那条。这在区块链中是一个很典型的问题,并且坏人也无计可施。
节点很容易产生不同长度的链,自然而言的最长的链条有最新的数据和最后一个区块。所以我们需要确保拥有最长的链条,这样做我们可以使用最新的区块链来覆盖原来的。
通过简单的长度比较来实现这个功能:
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}
恭喜!我们已经完成了所需要的基本函数,接下来我们想要一种简单的方法来查看和添加新区块。
### web服务
我们假设你已经很熟悉web服务是如何工作的并且会使用GO来实现,接下来将使用mux
包来创建web服务:
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
}
这里使用了之前在.env
文件中定义的端口,并且通过log.Println
输出一条消息来表明服务已经运行。
现在我们需要实现makeMuxRouter
函数,为了实现在浏览器中读和写我们的区块链,我们需要2个简单的路由规则。GET
请求来查看POST
请求来创建新区块。
func makeMuxRouter() http.Handler {
muxRouter := mux.NewRouter()
muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
return muxRouter
}
处理GET请求的函数如下:
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))
}
我们简单的使用json格式返回全部的区块链,当访问浏览器的localhost:8080
端口时候我们可以看到结果。 如果你修改了.env
文件中的ADDR
,确保你访问的端口正确。
POST请求相对复杂一点,我们需要一个新结构体叫做Message
,晚点解释为什么需要它:
type Message struct {
BPM int
}
处理这种请求的函数如下,我们晚点解释:
func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
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()
newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], m.BPM)
if err != nil {
respondWithJSON(w, r, http.StatusInternalServerError, m)
return
}
if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
newBlockchain := append(Blockchain, newBlock)
replaceChain(newBlockchain)
spew.Dump(Blockchain)
}
respondWithJSON(w, r, http.StatusCreated, newBlock)
}
因为我们需要使用新结构体来接受来自POST请求的JSON数据,这样我们就可以简单的通过发送POST请求并附带下面这种数据来生成新区块了:
{"BPM":50}
50是一个心跳次数的例子,你可以随意替换。
当我们从请求的body中解码出数据到var m Message
中,我们通过调用generateBlock
函数并传递前一个区块和心跳次数作为参数来创建一个新区块。然后通过isBlockValid
函数来进行验证。
2点说明:
spwe.Dump
是一个转换函数可以在命令行中输出好看的结构体,这对于调试十分有用。- 为了测试POST请求,我们喜欢使用postman,如果你喜欢使用终端
curl
也是不错的选择。
不管请求是否成功,我们都想收到通知。同样写一个函数来让我们知道发生了什么。记住,在GO语言中,永远不要忽略了error
,要 优雅的处理它们。
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)
}
接下来,让我们完成main
函数:
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
go func() {
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)
}()
log.Fatal(run())
}
这个函数都做什么了呢?
godotenv.Load()
可以让我们从根目录.env
文件中读取类似端口这种变量,这样就不用硬编码了。genesisBlock
是main
函数中最重要的部分,我们需要一个创世块,否则新的区块链就没有前一个区块的Hash了。- 我们分离了生成区块的代码到goroutine中,也就是分离区块链逻辑和web服务逻辑。
哈!完成了!
完整代码在 这里。
娱乐时间到,让我们试试。
打开终端并运行go run main.go
,我们可以看到web服务启动并生成了一个区块:
在浏览器中访问本机端口,我们使用8080,同样的我们看到相同的区块:
接下来发送post请求生成区块:
刷新浏览器,我们将看到区块链中有新的区块并且PrevHash
值等于老区块的Hash
值,一切都在预料中!
下一步
恭喜,你已经完成了有Hash和验证功能你的区块链!你现在可以去探索更高级的主题比如:工作量证明、权益证明、智能合约、分布式app、边链等等。这篇文章并没有处理结合工作量证明挖矿的问题。这应该写一个单独的文章,并且很多区块链并没有使用工作量证明。此外网络广播部分现在是通过web服务模拟的,本文中没涉及到P2P相关知识。