在本教程中,我将尝试通过帮助您以Go语言编写一个简单的区块链来揭开区块链的神秘面纱。
从本教程中,您应该能够:
- 理解blockchain术语
- 创建一个属于您自己的简单区块链
- 了解什么是块以及如何创建块
- 了解如何维护区块链的完整性
您可以在GitHub Repo中找到本教程的源代码。
区块链【Blockchain】:一种记录的数字分类帐,并按块【block】排列。
这些块通过加密哈希相互链接, 每个块包含指向前一个块的哈希。
区块链对于加密货币很有用,因为它具有分散性,这意味着存储的数据不在一个位置,而是每个人都可以访问,同时,任何人都不能改变。
构建一个简单的区块链
在本教程中,我们将为图书馆系统创建一个示范性的区块链。我们的区块链将存储包含图书借阅数据信息的块【block】。该实现的流程如下:
- 添加一本新书
- 为一本书创建一个Genesis块
- 将借阅数据添加到区块链
这是一个单节点,非复杂的区块链,在运行时将所有内容存储在内存中。
块【block】
在区块链中,一个块【block】中存储的是有价值的信息。这些信息可以是实现区块链的系统所需的事务或其他信息——例如,事务时间戳,或来自前一个块的散列哈希。
我们将继续为每个块【block】定义数据模型,以及组成区块链的借阅信息:
package main
// Block contains data that will be written to the blockchain.
type Block struct {
Pos int
Data BookCheckout
Timestamp string
Hash string
PrevHash string
}
// BookCheckout contains data for a checked out book
type BookCheckout struct {
BookID string `json:"book_id"`
User string `json:"user"`
CheckoutDate string `json:"checkout_date"`
IsGenesis bool `json:"is_genesis"`
}
// Book contains data for a sample book
type Book struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
PublishDate string `json:"publish_date"`
ISBN string `json:"isbn:`
}
在Block结构体中,Pos保存的是数据在链中的位置。Data则是该块【block】中包含的有价值的信息(在本例中指的是借阅条目)。Timestamp包含块【block】创建时的当前时间戳。Hash是块【block】生成的散列哈希。PrevHash存储的是前一个块的散列哈希。
定义了Block结构体后,我们需要考虑如何散列哈希这些块【block】。散列哈希用于标识和保持块【block】的正确顺序。计算哈希是区块链的一个非常重要的特性。计算哈希是一项困难的操作(在计算方面)。让散列哈希创建变得困难,这是一个经过深思熟虑的架构设计决策,因为它使添加新块【block】变得困难,从而防止在添加新块【block】之后发生可变操作。
散列哈希和生成块
我们将从一个简单的哈希方法开始,编写一个calculateHash函数来连接块【block】中的属性字段,并创建一个SHA-256的哈希值:
func (b *Block) generateHash() {
// get string val of the Data
bytes, _ := json.Marshal(b.Data)
// concatenate the dataset
data := string(b.Pos) + b.Timestamp + string(bytes) + b.PrevHash
hash := sha256.New()
hash.Write([]byte(data))
b.Hash = hex.EncodeToString(hash.Sum(nil))
}
接下来,我们编写一个CreateBlock函数,用于创建一个新的块【block】:
func CreateBlock(prevBlock *Block, checkoutItem BookCheckout) *Block {
block := &Block{}
block.Pos = prevBlock.Pos + 1
block.Timestamp = time.Now().String()
block.Data = checkoutItem
block.PrevHash = prevBlock.Hash
block.generateHash()
return block
}
CreateBlock函数的作用和它的声明完全一样——即创建一个新块【block】。该函数需要两个参数——①前一个块【block】②要添加的借阅项。您应该已经注意到,为了简单起见,我们没有对参数进行任何形式的检查。
创建区块链
我们已经为块【block】创建了结构体,并为创建块【block】编写了相应的函数。下面我们将实现一个区块链【blockchain】来保存关于这些块的列表,以及一个用于向区块链【blockchain】中添加块【block】的函数。
// Blockchain is an ordered list of blocks
type Blockchain struct {
blocks []*Block
}
// BlockChain is a global variable that'll return the mutated Blockchain struct
var BlockChain *Blockchain
// AddBlock adds a Block to a Blockchain
func (bc *Blockchain) AddBlock (data BookCheckout) {
// get previous block
prevBlock := bc.blocks[len(bc.blocks)-1]
// create new block
block := CreateBlock(prevBlock, data)
bc.blocks = append(bc.blocks, block)
}
创世纪块【Genesis Block】
在区块链【Blockchain】中,创世纪块【Genesis Block】是链中的首项。要添加新块【block】,必须首先检查是否有块【block】存在。如果没有,就创建一个创世纪块【Genesis Block】。下面让我们编写一个函数来创建一个新的创世纪块【Genesis Block】。
func GenesisBlock() *Block {
return CreateBlock(&Block{}, BookCheckout{IsGenesis: true})
}
We also need to write a function to create a new blockchain:
func NewBlockchain() *Blockchain {
return &Blockchain{[]*Block{GenesisBlock()}}
}
NewBlockchain函数返回带有创世纪块【Genesis Block】的区块链【Blockchain】结构体。由于我们没有考虑区块链【Blockchain】的数据持久性(这超出了本教程的范围),所以每当程序运行时,我们总是通过生成创世纪块【Genesis Block】来开启一个新的组。
Validation
在运行我们的区块链应用程序之前,我们需要以某种方式实现一个验证功能,这样在已经发生突变时(即有不法分子在书记借阅信息被保存的处理过程中,修改了借阅信息),区块就不会被保存。我们将创建一个工具函数validBlock,并在Blockchain结构体的AddBlock方法中使用它:
func validBlock(block, prevBlock *Block) bool {
// Confirm the hashes
if prevBlock.Hash != block.PrevHash {
return false
}
// confirm the block's hash is valid
if !block.validateHash(block.Hash) {
return false
}
// Check the position to confirm its been incremented
if prevBlock.Pos+1 != block.Pos {
return false
}
return true
}
func (b *Block) validateHash(hash string) bool {
b.generateHash()
if b.Hash != hash {
return false
}
return true
}
现在,我们的AddBlock方法就像下面这样了:
func (bc *Blockchain) AddBlock (data BookCheckout) {
// get previous block
prevBlock := bc.blocks[len(bc.blocks)-1]
// create new block
block := CreateBlock(prevBlock, data)
// validate integrity of blocks
if validBlock(block, prevBlock) {
bc.blocks = append(bc.blocks, block)
}
}
到目前为止,我们已经编写了区块链的主要部分!让我们创建一个web服务器,这样我们就可以与区块链通信并测试它。
在我们的main函数中,我们将编写用于创建web服务器和注册路由所需的代码,以便与区块链方法通信。我们将使用Gorilla Mux 来创建和路由我们的服务器:
func main() {
// register router
r := mux.NewRouter()
r.HandleFunc("/", getBlockchain).Methods("GET")
r.HandleFunc("/", writeBlock).Methods("POST")
r.HandleFunc("/new", newBook).Methods("POST")
log.Println("Listening on port 3000")
log.Fatal(http.ListenAndServe(":3000", r))
}
在我们的main函数中,我们定义了一个路由器【router】和三个路由【route】及其相应的处理程序【handler】。下面我们将着手创建这些处理程序:
getBlockchain处理程序将会简单的讲区块链以json的形式返回给浏览器:
func getBlockchain(w http.ResponseWriter, r *http.Request) {
jbytes, err := json.MarshalIndent(BlockChain.blocks, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(err)
return
}
// write JSON string
io.WriteString(w, string(jbytes))
}
writeBlock处理程序则使用传入的数据,向区块链中插入一个块【block】:
func writeBlock(w http.ResponseWriter, r *http.Request) {
var checkoutItem BookCheckout
if err := json.NewDecoder(r.Body).Decode(&checkoutItem); err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("could not write Block: %v", err)
w.Write([]byte("could not write block"))
return
}
// create block
BlockChain.AddBlock(checkoutItem)
resp, err := json.MarshalIndent(checkoutItem, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("could not marshal payload: %v", err)
w.Write([]byte("could not write block"))
return
}
w.WriteHeader(http.StatusOK)
w.Write(resp)
}
最后,我们来写newBook处理程序,用于创建新的Book数据:
func newBook(w http.ResponseWriter, r *http.Request) {
var book Book
if err := json.NewDecoder(r.Body).Decode(&book); err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("could not create: %v", err)
w.Write([]byte("could not create new Book"))
return
}
// We'll create an ID, concatenating the ISDBand publish date
// This isn't an efficient way but it serves for this tutorial
h := md5.New()
io.WriteString(h, book.ISBN+book.PublishDate)
book.ID = fmt.Sprintf("%x", h.Sum(nil))
// send back payload
resp, err := json.MarshalIndent(book, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("could not marshal payload: %v", err)
w.Write([]byte("could not save book data"))
return
}
w.WriteHeader(http.StatusOK)
w.Write(resp)
}
在编写了所有三个处理程序后,让我们清理我们的main函数。 我们的主要功能应如下所示:
func main() {
// initialize the blockchain and store in var
BlockChain = NewBlockchain()
// register router
r := mux.NewRouter()
r.HandleFunc("/", getBlockchain).Methods("GET")
r.HandlerFunc("/", writeBlock).Methods("POST")
r.HandlerFunc("/new", newBook).Methods("POST")
// dump the state of the Blockchain to the console
go func() {
for _, block := range BlockChain.blocks {
fmt.Printf("Prev. hash: %x\n", block.PrevHash)
bytes, _ := json.MarshalIndent(block.Data, "", " ")
fmt.Printf("Data: %v\n", string(bytes))
fmt.Printf("Hash: %x\n", block.Hash)
fmt.Println()
}
}()
log.Println("Listening on port 3000")
log.Fatal(http.ListenAndServe(":3000", r))
}
万事俱备
有了更新的代码,让我们启动应用程序:go run main.go
转到http://localhost:3000。你会看到创世纪块的显示:
让我们先添加一本新书,这样我们就可以使用该书的ID来向区块链中添加块【block】。 我将从终端使用cURL。 Postman当然没也是一个很好的工具:
$ curl -X POST http://localhost:3000/new \
-H "Content-Type: application/json" \
-d '{"title": "Sample Book", "author":"John Doe",
"isbn":"909090","publish_date":"2018-05-26"}'
在创建一本新书之后,我们将得到一个响应,响应报文中带有新建的Boo的ID。该ID是创建块【block】时不可或缺的一部分
现在已经有书了,我们下面来添加一笔借阅记录,我们向根端点http://localhost:3000发送一个POST请求,其中包含借阅的有效负载:
$ curl -X POST http://localhost:3000 \
-H "Content-Type: application/json" \
-d '{"book_id": "generated_id", "user": "Mary Doe",
"checkout_date":"2018-05-28"}'
刷新浏览器,我们就能在区块链信息中看到我们的借阅信息了:
祝贺你
我们成功了! !
恭喜,朋友!你已经走了很长的路。您刚刚编写了您的第一个区块链原型!!!值得注意的是,与上面的实现相比,真实的区块链要复杂得多。本教程中的实现使添加新块变得非常容易,但实际情况并非如此。添加新块需要进行一些繁重的计算(工作证明)。
通过对概念的解释,您应该对区块链有了更深入的理解。但还有一些其他的主题是理解区块链基础的先决条件,如股权证明(Proof of Stake ),工作证明(Proof of Work),智能合约(Smart Contracts),DApps等。
您可以在GitHub Repo上获得本教程的源代码。