用Go构建一个简单的区块链

在本教程中,我将尝试通过帮助你在Go中编写简单的区块链来揭开区块链的广义概念。

在本教程中,你应该能够:

  • 理解区块链术语。
  • 创建自己的简单区块链。
  • 了解什么是区块以及如何创建块。
  • 了解如何维护区块链的完整性。

区块链:一种数字分类帐,以较小的集合排列,称为块。这些块通过加密hash相互链接。每个块包含指向前一个块的哈希。区块链对于加密货币很有用,因为它具有去中心化的特性,这意味着存储的数据不在一个位置,而是每个人都可以访问,同时也是任何人都不可信的。

构建一个简单的区块链

在本教程中,我们将为图书馆系统创建一个示例区块链。我们的区块链将存储包含图书结账活动数据的区块。此实现的流程如下:

  • 添加一本新书。
  • 为书创建Genesis块。
  • 将Checkout数据添加到区块链。

这是一个单节点,非复杂的区块链,在运行时将所有内容存储在内存中。

在区块链中,块存储有价值的信息。此信息可以是实现区块链的系统所需的交易或一些其他信息,例如交易时间戳或来自前一个块的哈希。我们将继续为每个块定义数据模型,以及构成区块链的结账信息:

package main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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保持链中数据的位置。数据是块中包含的有价值信息(在这种情况下是结帐项目)。时间戳保存块创建的当前时间戳。哈希是块的生成哈希。PrevHash存储前一个块的哈希值。

在定义了Block结构的情况下,我们需要考虑对块进行哈希。哈希用于以正确的顺序识别和保持块。计算哈希值是区块链的一个非常重要的特征。计算哈希值是一项困难的操作(计算方面)。创建哈希的难度是经过深思熟虑的体系结构设计决策,因为它会增加新块的难度,防止在添加后进行可变操作。

哈希和生成块

我们将从一个简单的哈希方法开始,并编写一个函数calculateHash来连接块字段并创建一个SHA-256哈希:

1
2
3
4
5
6
7
8
9
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来创建一个新块。

1
2
3
4
5
6
7
8
9
10
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创建了struct,并创建了一个创建一个的函数。我们将实现一个区块链来保存这些区块的列表,以及一个将区块链添加到区块链的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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块

在区块链中,Genesis Block是链中的第一个项目。要添加新块,我们必须首先检查现有块。如果没有,则创建Genesis Block。让我们编写一个函数来创建一个新的Genesis Block。

1
2
3
4
5
6
7
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结构。由于我们没有考虑区块链的测量数据持久性,因为这将超出本教程的范围,所以我们总是从一个新的组开始,通过在程序运行时生成Genesis Block。

验证

在我们运行区块链应用程序之前,我们需要以某种方式实现验证,以便在已经发生变异时不保存块。我们将创建一个辅助函数validBlock并在附加到Blockchain结构的AddBlock方法中使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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方法应如下所示:

1
2
3
4
5
6
7
8
9
10
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来路由和创建服务器mux:

1
2
3
4
5
6
7
8
9
10
11
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))
}

在我们的主要功能中,我们有一个路由器和三个路由、处理程序定义。我们现在将创建这些处理程序。

getBlockchain处理程序将简单地将区块链作为JSON字符串写回浏览器:

1
2
3
4
5
6
7
8
9
10
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处理程序添加一个包含已发送数据的新块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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数据,因此我们将使用生成的ID作为块添加。记住流程:

  • 添加一本新书。
  • 为书创建Genesis块。
  • 将Checkout数据添加到区块链。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)
}

编写了所有三个处理程序后,让我们清理我们的主要功能。我们的主要功能应如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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。你会看到Genesis Block显示:

让我们添加一本新书,这样我们就可以在添加块中使用ID。我将从终端使用cURL。Postman也是一个很好的工具:

1
2
3
4
$ 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"}'

创建新书后,我们将使用生成的ID获取有效负载。请注意,因为它总是需要创建块。要添加一个将存储在区块链中的结帐记录,我们会向根端点http://localhost:3000发送一个POST请求,其中包含结帐项的有效负载:

1
2
3
4
$ curl -X POST http://localhost:3000 \
-H "Content-Type: application/json" \
-d '{"book_id": "generated_id", "user": "Mary Doe",
"checkout_date":"2018-05-28"}'

刷新浏览器,我们将看到添加了自己的哈希的新项目:

我们做到了!!

恭喜,伙计!你走了很长的路。你刚刚写了第一个Blockchain原型!值得注意的是,与我们上面的实现相比,实际的区块链要复杂得多。本教程中的实现使得添加新块变得非常容易,而实际情况并非如此。添加新块需要一些繁重的计算(如工作量证明)。

通过解释的概念,你应该更好地理解区块链。还有其他主题是了解区块链基础的先决条件,如股权证明,工作量证明,智能合约,DApps等。

你可以在此GitHub Repo上获取本教程的源代码。

======================================================================

分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:

  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • tendermint区块链开发详解,本课程适合希望使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

汇智网原创翻译,转载请标明出处。这里是原文用Go构建一个简单的区块链