/Blockchain

Building a Simple Blockchain with Golang

Primary LanguageGo

我用160行代码写出了个区块链...

完成本篇教程,你将会做出一条属于自己的区块链系统

你可以在自己的浏览器中显示自己的区块链系统,类似于下图所示

总览

很多人认识区块链是因为比特币,结实比特币也是缘于它攀升的价格,但作为技术人员,理应了解其本质。

区块链不仅仅是计算机科学,还涉及了政治经济制度,社会分工协作等等很多方面,因此我的关注点不仅在于深度,更在于其广度,更多是站在研究的角度。区块链是21世纪最具革命性的技术之一,而且这项技术还尚在发展中,仍然有很多潜力未曾展现。

在这篇文章通过160行代码使用Go语言编写自己的简单区块链,最后在web浏览器中可以打开来查看自己区块链。

在这篇文章中你可以学到

  • 创建自己的区块
  • 为每个区块添加哈希码
  • 为区块链提供Web服务
  • ......

为了使教程简便易懂,我们使用web服务替代P2P网络,因此我们可以在浏览器中查看新添加的block。

首先,确保你下载了Go语言安装包。再下载下面的三个包:

go get github.com/davecgh/go-spew/spew

go get github.com/gorilla/mux

go get github.com/joho/godotenv

首先第一个包,spew,可以让我们在控制台查看structslices理论上log包和fmt包也可以查看这些信息,但spew可以使结构更加清晰化。

第二个包,mux用于处理web服务的包,比起httpmux可以使web服务更加简便,最近也流行gin框架做web服务,大家也可以去试一试。

第三个包godotenv,从包的名字就可以看出,go do env,可以读取同一个目录下的env文件,这样子我们就无需对HTTP端口之类的内容进行编码。

开始

首先新建一个文件夹,在文件夹中创建一个main.go文件。根据第三个包godoenv,我们再创建一个.env文件。只需向此文件添加一行:

PORT=8080

意味着我们开启在main写入代码中的web服务监听8080端口。

好了,接下来的代码我们都将在main.go文件中构建。

首先的首先,我们先想一想我们想要构建一个区块链,我们到底需要一些什么?

  • Block
  • Blockchain
  • Data
  • test数据
  • web服务

在写代码之前进行一次构建对于整个编码过程都很重要!

所以我为以上问题做了一个图示,展示我们整个区块链架构的核心过程。

总代码架构图示

根据函数名称,应该可以大概了解函数的主要作用

  • 左侧的Block、Blockchain、Message就是我们所需要的定义组成区块链的每个块的结构。
  • generateBlock的作用是初始化每一个区块,caculateHash的作用是为每一个区块计算hash码。isBlockValid去校验我们构建的区块链是否正确。
  • makeMuxRouter启用Web服务,GET和POST我们的Data值。接下来run运行Web服务。

首先我们需要导入所需要的包,很简单

package main

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"
)

如何了解一个package

介绍一下如何去了解一个包吧。

很多人想要去了解一个包就先去别人的博客,自己在百度上随便找一个链接就以为自己吃透了包的内容。

但实际上,了解一个包最应该查看的资料就是官方资料,而且推荐是英文版,因为别人翻译过的资料,很可能有信息缺失,信息传递过程中有些信息就不见了,就好像吃别人吃剩的饭一样,难受死人了。

比如我们需要了解下面这个包的内容crypto/sha256我们首先应该去官网**了解,**首先打开Golang的官网,打开上方的packages你可以看到很多包,按ctrl + F去直接搜索sha256,你就找到了需要的包。

每个包都有Overview,Index和Examples,这个包里的内容是:
Package sha256 implements the SHA224 and SHA256 hash algorithms as defined in FIPS 180-4.

翻译过来的意思是:

sha256包实现了FIPS 180-4中定义的SHA224和sha256哈希算法。

在Index内容中可以查看包内的函数,Examples中含有应用例子,比如果我们需要创建一个哈希码:

点击New案例,你就会发现有一个Run按钮,直接运行,就会运行出现应有的哈希码。

以上就是大概了解一个包的内容,如果你想真正学会本篇的区块链教程,请你一定要自己查看每个包内的具体内容,这样再结合具体的代码实战,会让你对包的理解和使用更加流畅。
然后,我们需要一个Block用来写我们的每一个区块。

我们每一个区块中含有Index,Data,Timestamp,Hash,Prehhash。

  • Index是指索引,从0开始递增。
  • Data就是我们要传递的数据,
  • Timestamp是时间戳,记录我们提交Data所用的时间,
  • Hash是表示此数据记录的sha256标识符
  • Prehash记录上个块所构建哈希码,用sha256包创建哈希码。

数据模型

在main中Copy and paste以下代码

type Block struct {
	Index     int
	Timestamp string
	Data      int
	Hash      string
	PrevHash  string
}

这样我们的每一个区块就构成了。

这里你可能有一个疑惑:哈希是如何去识别区块和区块链呢?

答案是:哈希使用散列来识别和保持块的正确顺序。通过确保它们PrevHash中的每个BlockHash前面的相同,就像下图所示的一样。

这样Block我们知道组成链的块的正确顺序。

但我们每次直接使用Block会很麻烦,所以我们用变量Blockchain来构建我们的类型Block,这样在以后使用增加区块append时就很容易构建了。

再声明一个变量互斥锁,调用时就不需要在函数体内部再进行重新声明了。

还有需要定义一个类型是Message,用于提交Data时,便于解构代码。

var Blockchain []Block
var mutex = &sync.Mutex{}

type Message struct {
	Data int
}

生成哈希值和新区块

下面让我们写一个函数来获取我们的Block数据并创建一个SHA256哈希值。

//sha256 do a hash code for every Block
func caculateHash(block Block) string {
	record_block := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.Data) + block.PrevHash
	h := sha256.New()
	h.Write([]byte(record_block))
	return hex.EncodeToString(h.Sum(nil))
}

这个calculateHash函数将我们提供的块作为参数的索引、时间戳、Data和PrevHash连接起来,并以字符串的形式返回SHA256散列。我们在此函数中使用了strconv包,这个包的作用是方便我们转换格式,比如Itoa函数的格式是将Int格式转换为String格式,虽然很简单,但还是建议去官网查看一下包的内容。

很简单的代码,4-6行代码就是我们之前在官网中所看到的代码,也就是上面图片中的代码。

现在,我们可以用一个新的generateBlock函数生成一个新的Block,其中包含我们需要的所有元素。

func generateBlock(oldBlock Block, Data int) Block {
	var newBlock Block

	nowtime := time.Now()

	newBlock.Index = oldBlock.Index + 1
	newBlock.Timestamp = nowtime.String()
	newBlock.Data = Data
	newBlock.PrevHash = oldBlock.Hash
	newBlock.Hash = caculateHash(newBlock)

	return newBlock
}

我们使用time包中的Now函数来表示创建newBlock的时间。还要注意,调用了先前的calculateHash函数。PrevHash是从前一个块的hash复制过来的。Index从前一个块的索引中递增。

到此为止我们已经成功声明了一个区块,并且为它计算hash值。

数据核实

现在我们需要编写一些函数来确保块没有被篡改。我们通过检查Index来确保它们像预期的那样递增。我们还检查以确保我们的Prehash确实与前一个块的Hash相同。最后,我们希望通过在当前块上再次运行calculateHash函数来再次检查当前块的散列。让我们写一个isBlockValid函数来做所有这些事情并返回一个bool值,如果它通过了我们所有的检查,它将返回true。

func isBlockValid(newBlock, oldBlock Block) bool {
	if oldBlock.Index+1 != newBlock.Index {
		return false
	} else if oldBlock.Hash != newBlock.PrevHash {
		return false
	} else if caculateHash(newBlock) != newBlock.Hash {
		return false
	} else {
		return true
	}
}

现在,我们已经完成了构建区块链的大部分工作,现在我们想要一种方便的方式来查看我们的区块链并写入它,最好是在一个web浏览器中,这样我们就可以直观的展示我们的每一个区块的内容。

Web服务

如果你还不了解Go语言是如何启用Web服务,你可以先去Go语言的net/http包的官网去先了解一下,如果不了解也没关系,我会用很直白的话讲明白。

我们使用mux包来帮助我们构建Web服务,去mux的github官网,可以看到一下信息,帮助我们简单地构建一下web服务。

所以我们很简单调用一下函数(之后添加),来方便我们构建GET和POST方法。

//create web service
func makeMuxRouter() http.Handler {
	muxRouter := mux.NewRouter()
	muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
	muxRouter.HandleFunc("/", handleWriteBlockchain).Methods("POST")
	return muxRouter
}

这是我们的GET函数

//when receive Http request we write blockchain
func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {
	json, err := json.MarshalIndent(Blockchain, "", "  ")
	if err != nil {
		log.Fatal()
	}
	io.WriteString(w, string(json))
}

我们去json包的官网可以看到

MarshalIndent函数可以应用缩进来格式化输出。输出中的每个JSON元素将以新行开始,以前缀开头,然后根据缩进嵌套,后跟一个或多个缩进副本。

当然还有io包内的函数,需要你自己去官网中查看。

这是我们的POST函数

func handleWriteBlockchain(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	var msg Message

	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.Data)

	if isBlockValid(newBlock, prevBlock) {
		Blockchain = append(Blockchain, newBlock)
		spew.Dump(Blockchain)
	}
	mutex.Unlock()

	respondWithJSON(w, r, http.StatusCreated, newBlock)
}

现在你明白为什么要将Message单独作为一个结构了吗?我们使用单独的msg结构的原因是为了接收JSON POST请求的请求体,我们将使用该请求体来编写新的块。这允许我们简单地发送一个带有以下主体的POST请求,我们的处理程序将为我们填充块的其余部分。

在将请求体重新解码为var msg消息结构之后,我们通过将之前的块和新的Data传递给前面编写的generateBlock函数来创建一个新的块。这就是函数创建新块所需要的一切。我们使用前面创建的isBlockValid函数进行快速检查,以确保新块是符合要求的。

你还会发现我们使用了mutex的互斥锁内容,因为当添加新块时,是不允许其他函数进行访问的。

  • spew.Dump方便我们打印结构体到调试窗口
  • POST请求可以使用curlpostman工具进行调试

当我们的POST请求成功或失败时,我们希望得到相应的通知。我们使用一个小小的包装器函数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)
}

最后运行服务

//run Http serve
func run() error {
	myHandler := makeMuxRouter()
	httpPort := os.Getenv("PORT")
	log.Println("HTTP Server Listening on port :", httpPort)
	s := &http.Server{
		Addr:           ":8080",
		Handler:        myHandler,
		ReadTimeout:    10 * time.Second,
		WriteTimeout:   10 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	log.Fatal(s.ListenAndServe())

	return nil
}

在run函数中,也有我们需要了解的一下新知识比如http包的内容

大家可以根据之前我带大家去了解新包的方式去了解os包的内容

差不多完事了

最后是我们的main函数

让我们把所有这些不同的区块链函数、web处理程序和web服务器连接在一个简短的main函数中

//main func
func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal(err)
	}

	go func() {
		t := time.Now()
		genesisBlock := Block{}
		genesisBlock = Block{0, t.String(), 0, caculateHash(genesisBlock), ""}
		spew.Dump(genesisBlock)

		mutex.Lock()
		Blockchain = append(Blockchain, genesisBlock)
		mutex.Unlock()
	}()

	log.Fatal(run())
}

看看main函数做了什么。

还记得我们的.env文件吗?2-6行,就是编码.env文件,方便我们监听8080端口。

7-16行,同时我们用并行方式运行匿名函数,genesisBlock是最重要的主要功能部分。我们需要给区块链提供一个初始的块,否则一个新的块将无法与它之前的哈希值进行比较,因为之前的哈希值不存在。

18行,最后运行web服务。

最后我们的区块链就构建完成了,我数了数也就159行。

但足够你用好几天时间去消化内部的知识了。

我们来运行一下试试看:-)

在终端启用go run main.go

我们看到web服务器已经启动并运行

打开浏览器访问localhost:8080我们看到了相同的创世区块。

接下来我通过postman工具进行POST{"Data":150}请求

多试几个看看?

在浏览器中访问

你已经完成本篇文章的全部内容了,可能你已经看完了整篇文章不知云里雾里,但我还是希望你能够亲手将上述所有的代码全部自我实现一遍。

欢迎点赞,关注哦!