前言
为什么说是最简单的区块链呢,因为根本写不出一个完整的区块链,甚至连区块链的Demo都算不上。本文充其量可以当做Go语言的一个入门教程,至少对我来说是这样。所以,即使读者没有任何区块链和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来测试一下效果。
// 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访问使用。
// 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,可以看到如下结果
此时只有创世区块。接下来添加一个交易
http://localhost:8888/blockchain/write?data=Send 1 BTC to Faye.
现在区块链变成了这样
每调用一次,区块链中就添加一个区块。
好了,到此为止,我们已经实现了预定的全部功能。
完整代码已上传GitHub,请点击jingedawang/BlockChainDemo下载。
总结
区块链并没有想象中那么神秘,也不是无所不能。最初版本的区块链有很大局限性,所以才有了区块链2.0、3.0,以及以太坊、智能合约的出现。想真正了解区块链,不深入到行业应用中是不行的,所以本文只是浅尝辄止,抛砖引玉而已。鉴于当下区块链工程师年薪百万,我想,是时候考虑转行了:-)
参考资料
区块链技术核心概念与原理讲解 慕课网
用GO语言构建自己的区块链 慕课网
Go 语言教程 菜鸟教程
golang 函数以及函数和方法的区别 D_Guco
有疑问加站长微信联系(非本文作者)