本文翻译自《Code your own blockchain in less than 200 lines of Go!》,国内被墙无法访问。由于我的英语水平有限,所以也不逐段翻译,而是概括其关键内容。
为了简单起见,本文内容不包括一致性算法如POW、POS,网络广播等内容。
因为我们要用GO语言实现,所以这里假设各位已经有了一定go语言编程的基础,首先我们需要下载几个模块
go get github.com/devecgh/go-spew/spew
spew可以理解为一种结构化输出工具,可以让我们更清晰地查看structs 和slices数据。
go get github.com/gorilla/mux
Gorilla/mux 是一个用于Web开发的组件
go get github.com/joho/godoenv
Gotdotenv 是一个读取在项目根目录的.env
文件中的配置信息的组件,本文中我们的web服务端口就定义在该文件中
现在我们创建一个.env
文件并写入
ADDR=8080
创建一个main.go
文件,我们所有的代码将会写入该文件中,并且不会多于200行。
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"
)
type Block struct {
Index int
Timestamp string
BPM int
Hash string
PrevHash string
}
Block用于保存要写入的区块链中的数据,每一个字段的意思如下:
- Index 表示区块号(区块高度)
- Timestamp 自动产生的表示该块的生成时间
- BPM 可以理解为写入区块的数据
- Hash 本块的哈希值
- PrevHash 前一个块的哈希值
然后创建一个Block的数组切片用于代表区块链
var Blockchain []Block
为什么要对数据进行哈希化,有两个主要原因
- 节省空间,哈希值由区块中所有数据计算而来,在我们的例子中,我们每个块中都只有很少量的数据,但假如我们的区块中有成百上千条数据,我们用其哈希值做标识明显更有效率,你总不能在随后的块中保存前面块的所有数据吧?
- 保护区块完整性,像我们这样保存前一个区块的哈希值,我们就很容易的可以检查到前一个区块有没有被篡改(因为篡改后哈希值变化,要么是前一个块自检无法通过,后边的块永远会指向正确的前一个块,而不会指向被恶意篡改后的。)
我们现在实现一个计算哈希值的方法
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)
}
我们使用到了Index、Timestamp、BPM、PrevHash 字段用于计算当前块的哈希值
我们再写一个用于产生新块的方法
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
}
代码很简单,就不废话了。
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 replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}
这里假设您已经熟悉,并有一定的GO 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
}
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))
}
我们将整个Blokcchain转换为JSON串作为GET请求的返回值 。
我们使用POST请求添加新块,求稍微复杂一些,我们需要一个结构体
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)
}
我们上边使用了一个封装后的方法respondWithJson
当出现错误的时候返回HTTP:500,成功的话正常返回。
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())
}
go run main.go
使用浏览器访问我们的web服务,我们可以看到如下结果
我们使用POSTMAN测试添加新区块
我们刷新一下浏览器,我们就可以看到我们刚添加的块
这最后一段看得懂,但翻译好麻烦,我决定给上英文原文了。
Congrats!! You just wrote up your own blockchain with proper hashing and block validation. You should now be able to control your own blockchain journey and explore more complicated topics like Proof of Work, Proof of Stake, Smart Contracts, Dapps, Side Chains and more.
What this tutorial doesn’t address is how new blocks get mined using Proof of Work. This would be a separate tutorial but plenty of blockchains exist without Proof of Work mechanisms. In addition, the network broadcasting is currently simulated by writing and viewing the blockchain in a web server. There is no P2P component in this tutorial.
下面链接是我转成PDF的英文原文有兴趣大家可以阅读一下。
《Code your own blockchain in less than 200 lines of Go!》