MENU

go小型区块链开发

April 1, 2019 • go阅读设置

引子

本文使用Go语言实现了

  • 区块的定义和构建
  • 区块链的定义和构建
  • 添加交易
  • 查看区块链内容
  • 提供Go API和Web API两种方式

区块链的概念

区块链源自比特币,当年中本聪计划打造一个完全去中心化的电子货币交易系统,区块链应运而生。发明区块链的动机,大概是中本聪觉得,任何中心化的系统都不够安全,一旦把特权赋予某些人,就存在滥用职权和腐败的可能。只有在去中心化的系统中,才会存在绝对的安全。
去中心化其实很简单,直接让每一个节点都保存完整的交易信息,自然就不需要中心节点了。但这样会造成资源的极大浪费,也会造成通信的拥堵。不过比特币似乎就是这样干的,毕竟比特币的总量不算很多,交易量也不至于太大。
去中心化的另一个问题是需要共识机制。因为区块链是一个单向链表,如果两个节点同时想要向链表头部添加元素,势必造成链表的分叉。共识机制规定了整个网络中最长的那个链为有效链,任何节点一旦发现其它链比本地保存的链更长,就必须更换为那个最长的链。
此外,节点是不能随意向区块链中添加元素的,否则谁添加的最快,谁的区块链就最长,那岂不是成了速度竞赛。制约的方法是,一个节点产生的交易,必须由另一个节点记账,才能加入区块链中。为了鼓励人们积极为其他人记账,中本聪设计了“工作量证明”这一环节,也就是我们俗称的“挖矿”。成功为他人记账的节点,会收到若干个比特币的奖励。这样一来,人们蜂拥而至,争先为别人记账,但交易数量有限,多个人同时记账只会有一个人记账成功,其他人无功而返。为了使这一过程不受网速的影响,使大家能够公平竞争,中本聪规定,记账者必须得到若干个0开头的账单摘要才算记账成功。所谓账单摘要,就是把账单数据按照规定的组合方式,加上随机数,再经过某种哈希算法(比如SHA256)得到的固定长度的数据串。目前的规定是以18个0开头,每个0是一个16进制数字,也就相当于平均每尝试1618次才可能得到一个符合条件的账单摘要。这也是为什么挖矿需要消耗大量的计算资源,而且挖矿会越来越难(数字0开头的数量还会进一步增多)。
说了这么多理论,也该开始实践了。但说着容易做着难,我们没法把上面提到的所有特性都实现出来,只能实现最基础的功能。即使如此,对帮助大家直观地了解区块链也已经足够了。

Go语言实现区块链

如果手边有一台电脑,建议按照本节的流程亲自敲一遍代码。

0.配置开发环境

本来想把如何配置开发环境写一写,但实在太繁琐,又难以满足不同系统用户的需求,遂作罢。大家只能发挥自己的聪明才智搞一搞了。

1. 定义区块

// file: Block.go
package core

import (
    "crypto/sha256"
    "encoding/hex"
    "time"
)

type Block struct {

    // Block header
    Index int64
    Timestamp int64
    PrevBlockHash string
    Hash string

    // Block data
    Data string
}

func createBlock(prevBlock Block, data string) Block{
    newBlock := Block{}
    newBlock.Index = prevBlock.Index + 1
    newBlock.Timestamp = time.Now().Unix()
    newBlock.PrevBlockHash = prevBlock.Hash
    newBlock.Data = data
    newBlock.Hash = calculateHash(newBlock)
    return newBlock
}

func calculateHash(block Block) string {
    toBeHashed := string(block.Index) + string(block.Timestamp) + block.PrevBlockHash + block.Data
    hashInBytes := sha256.Sum256([]byte(toBeHashed))
    return hex.EncodeToString(hashInBytes[:])
}

这段代码定义了一个结构体Block,用来表示一个区块。区块包含区块头和数据两部分,其中,区块头包括序号Index、时间戳Timestamp、上一区块的摘要PrevBlockHash以及当前区块的摘要Hash。数据为了简单起见,直接用string类型表示。
下面两个私有函数分别用来创建区块和计算给定区块的摘要。之所以称为私有函数,是因为函数名以小写字母开头,Go编译器自动按照函数名首字母的大小写决定该函数的访问级别。在calculateHash函数中,我们把序号、时间戳、上一区块的摘要以及数据连接成一个长字符串,并计算该字符串的哈希值,作为当前区块的摘要。需要注意,Go语言中声明变量可以不显式指定类型,但需要用:=符号来初始化

2. 定义区块链

// file: BlockChain.go
package core

import "fmt"

type BlockChain struct {
    Blocks []*Block
}

func CreateBlockChain() BlockChain {
    genesisBlock := createBlock(Block{Index:-1}, "I am genesis block.")
    blockChain := BlockChain{}
    blockChain.Blocks = append(blockChain.Blocks, &genesisBlock)
    return blockChain
}

func (blockChain *BlockChain) AddTransaction(data string)  {
    block := createBlock(*blockChain.Blocks[len(blockChain.Blocks) - 1], data)
    blockChain.Blocks = append(blockChain.Blocks, &block)
}

func (blockChain *BlockChain) Print()  {
    for _, block := range blockChain.Blocks {
        fmt.Printf("Index: %d\n", block.Index)
        fmt.Printf("Timestamp: %d\n", block.Timestamp)
        fmt.Printf("PreBlockHash: %s\n", block.PrevBlockHash)
        fmt.Printf("Hash: %s\n", block.Hash)
        fmt.Printf("Data: %s\n", block.Data)
        fmt.Println()
    }
}

这里,我们把区块链定义为另一个结构体,内部包含区块的数组切片。Go语言中的数组切片,其实就是变长数组,它的容量依赖于实际数组的长度。
我们提供了三个公有函数。CreateBlockChain用来创建一个新的区块链,在该函数中自动创建了一个区块,称为“创世区块”,该区块不含任何数据,Index为0,只用于标识区块链的起点。AddTransaction函数用来添加交易,内部会创建一个新的区块,并链接到区块链上。Print函数用来打印完整的区块链信息。
细心的话可以发现,这里的函数名前面增加了一些内容。在Go语言中,这种函数称为方法,方法名前面的部分是接收者,类似于C++中的this指针。AddTransaction和Print方法都声明了BlockChain类型的接收者,于是这两个方法可以当做BlockChain类型的成员函数来使用。

3. Go API Demo

以上已经实现了区块链的核心功能。我们现在写一个demo来测试一下效果。

go// file: main.go
package main

import (
    "BlockChainDemo/core"
)

func main()  {
    blockChain := core.CreateBlockChain()
    blockChain.AddTransaction("Send 1 BTC to Faye.")
    blockChain.AddTransaction("Send 2 BTC to Liling.")
    blockChain.Print()
}

Go语言的主函数必须位于main包中,否则不能执行。运行该程序,输出结果为

Index: 0
Timestamp: 1538731505
PreBlockHash: 
Hash: f8970ec722193096998452516c709c1890323e5df5bd7cf1e139b9c592394f6d
Data: I am genesis block.

Index: 1
Timestamp: 1538731505
PreBlockHash: f8970ec722193096998452516c709c1890323e5df5bd7cf1e139b9c592394f6d
Hash: 12f8ca6f66b0b21e6d1ed7682265f849f46e4a4ff10e6ba794021eb25d0ab033
Data: Send 1 BTC to Faye.

Index: 2
Timestamp: 1538731505
PreBlockHash: 12f8ca6f66b0b21e6d1ed7682265f849f46e4a4ff10e6ba794021eb25d0ab033
Hash: 1c849f10a0da4f2aa45275f1585ddc700fa46c24cb8162484bf628a16aeac5a0
Data: Send 2 BTC to Liling.

可以看到,添加两次交易后,区块链的长度变为3,除了创世节点,后面每个节点表示一次交易。每个区块的Hash值与当前区块和上一区块都有关,因此任何人都无法篡改历史数据,任何微小的改动都会导致后面链条中所有数据的变化。

4. Web API Demo

最后一部分,发挥Go语言强大的Web编程能力,我们提供一个Web API供HTTP访问使用。

go// file: server.go
package main

import (
    "BlockChainDemo/core"
    "encoding/json"
    "io"
    "net/http"
)

var blockChain core.BlockChain

func run()  {
    http.HandleFunc("/blockchain/get", blockchainGetHandler)
    http.HandleFunc("/blockchain/write", blockchainWriteHandler)
    http.ListenAndServe("localhost:8888", nil)
}

func blockchainGetHandler(writer http.ResponseWriter, request *http.Request) {
    bytes, error := json.MarshalIndent(blockChain, "", "\t")
    if error != nil {
        http.Error(writer, error.Error(), http.StatusInternalServerError)
        return
    }
    io.WriteString(writer, string(bytes))
}

func blockchainWriteHandler(writer http.ResponseWriter, request *http.Request)  {
    data := request.URL.Query().Get("data")
    blockChain.AddTransaction(data)
    blockchainGetHandler(writer, request)
}

func main()  {
    blockChain = core.CreateBlockChain()
    run()
}

实现方法非常简单,设置两个回调函数,对应两个URL,一个用来查询当前区块链,另一个用来向区块链中添加交易。运行此程序,然后打开浏览器,访问http://localhost:8888/blockchain/get 可以看到如下结果

api地址

get一个地址
url = 'http://localhost:8888/blockchain/get'
-w993

write一个区块
url = 'http://localhost:8888/blockchain/write?data=Send%201%20BTC%20to%20XXY.
'
-w993

每调用一次,区块链中就添加一个区块。

完整代码已上传GitHub,请点击 gochain 下载

Leave a Comment