背景 : 最近在学习区块链相关知识, 主要参考了 hackernoon.com/learn-block... 这个教程, 教程中使用了 python 实现了一个简单的区块链。因为 golang 是区块链的主流语言,于是我在彻底理解了相关概念后使用 golang 又实现了一遍,并且对里面的少部分代码逻辑进行了重构显得更加清晰明了。看完此文,你将彻底理解区块链的关键特性包括; 不可篡改性,如何往区块链中添加交易信息, 如何实现工作量证明, 如何记账(挖矿),如何实现多节点一致性, 如何去中心化,并且使用golang从0实现一个区块链 。 希望你有密码学的基础知识, 和一致性算法的基础知识。如果没有,可以参考我的另外两篇博客 Raft 算法选主详解与复现, 完成 MIT 6.824(6.5840) Lab2A 和 看完符合安全厂商标准的密码体系,再看看你公司的密码体系够安全吗
前置知识
区块链是什么?
就像上面这幅图一样,区块链是一个个块组成的链。块称作 Block, Block 形成的链称作 Block Chain. Block 里面大概有这几种数据:
- index, 表示区块链的索引,是第几个块
- timestamp, 生成这个块的时间
- transaction, 这个块里面的交易信息,这里是一个list, 因为一个块里面可能会有多个交易信息, transaction 里面又有3部分: sender 表示转账方, recipient 表示接收方, amount 表示金额。 transaction 也被称作是账本, 是整个块中真正有价值的信息
- pow, proof of work, 工作量证明, 谁先算出这个 pow, 谁就有资格对这个块进行打包。 计算 pow 然后打包 的这个过程也称作记账, 记账完成后有奖励。为了获取奖励而进行记账,这就叫做挖矿
- prevHash, 对前一个块的所有内容进行hash
上面这么讲,是比较偏向于从数据结构方面进行理解,下面来分析下这种数据结构可以推导出 区块链的什么性质
区块链性质
为什么要记账(挖矿)
再重复一次,记账也就是对交易信息进行打包。在区块链中,每一个节点手上都一个链,当一个节点发生交易的时候,这个节点就会对周围节点进行广播,收到广播后的节点就可以对这笔交易进行打包。为什么要打包呢? 因为打包有奖励? 为什么又奖励呢? 因为打包过程需要消耗计算资源,通常是一个很复杂的密码学操作。通过解决密码学难题,从而竞争获得唯一的记账权。 一旦某个节点记账成功 ,其他节点则复制这个结果。
不可篡改性
不可篡改性可以从两方面来解释:
链路的逐次哈希过程
因为每一个块中都有 上一个 哈希值, 所以你要想改变中间的某一个数据,那么后面的所有哈希值都会对不上,别的节点一旦对你的链路进行交易就会发现你的块是被篡改的
链路的逐次工作量证明
每个块的工作量证明都是和上一个块的哈希有关的,如果某个节点想篡改中间的某个交易,那么这个块的以及这个块后面哈希都会变,这个块和这个块后面的所有 工作量证明就会作废,需要重新计算工作量证明。前面说过这个工作量证明是一个非常消耗计算资源的密码学难题的,对一个区块打包都非常困难,更何况对篡改的块之后所有的块进行打包
一致性维护
在区块链中每个节点都保存一个区块链,那么如何维护所有节点的一致性呢? 也很简单,所有节点会以最长的那条链为准。这就好比 raft 算法的选主过程, 所有节点都会给 term 最大的节点投票, 所有节点都听 leader 发号施令类似。
区中心化
基于上面的分布式区块链来说,所有节点的地位是一样的,并不存在某个节点可以控制其他的节点的情况。挖矿完成的节点可以看作是一个临时的leader, 大家都以它的最长链路为准,但这个节点的权限相对于中心化的系统来说也是非常小的。
用 golang 实现一个区块链
接下来,我将使用 golang 从0实现一个区块链,包括了以下功能
- 区块链的基本组成部分实现
- 往链中添加交易,只添加交易,但不进行打包
- 对交易进行打包,工作量证明计算, 获取奖励,对外来说就是挖矿
- 往当前节点中注册其他节点
- 一致性校验,往其他节点发送请求以获取其他节点的区块链,然后对其有效性进行校验,若通过校验且其他节点的链更长,则更新自己区块链
创建工程目录
创建 block_chain_demo 文件夹,命令行输入:
shell
go mod block_chain_demo
go mod tidy
文件夹目录如下:
shell
block_chain_demo
block_chain
-- block_chain // 区块链代码
main
-- main.go // main 函数 以及 调用区块链的 http 接口
-- go.mod
实现区块链
block_chain.go
在 block_chain.go 文件我们定义了区块链的所有操作
BlockChain 成员
go
package block_chain
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
type BlockChain struct {
NodeIdentifier string
Chain []Block
Nodes []string
CurrentTransactions []Transaction
}
type Transaction struct {
Sender string `json:"sender"`
Recipient string `json:"recipient"`
Amount uint `json:"amount"`
}
type Block struct {
Index int `json:"index"`
TimeStamp int64 `json:"timeStamp"`
Transactions []Transaction `json:"transactions"`
Pow uint64 `json:"pow"`
PrevHash string `json:"prevHash"`
}
在BlcokChain结构体中定义了区块链中所有的成员属性,包括:
- NodeIdentifier 表示当前链的id
- Chain, 区块链本体, 由一个个block组成
- Nodes, 当前区块链节点的邻居节点集合
- CurrentTransaction, 当前区块链中未打包的交易信息
剩下的 Transaction 和 Block 在前面的章节已经讲过了,这里不再赘述
BlcokChain 方法
go
// constructor
func (b *BlockChain) Init(nodeIdentifier string) {
b.Nodes = []string{}
firstBlock := Block{
Index: 0,
TimeStamp: time.Now().Unix(),
Transactions: nil,
Pow: 1,
PrevHash: "1",
}
b.Chain = append(b.Chain, firstBlock)
}
// register node
func (b *BlockChain) RegisterNode(node string) {
b.Nodes = append(b.Nodes, node)
}
// get nodes
func (b *BlockChain) GetNodes() []string {
return b.Nodes
}
// get block hash
func (b *BlockChain) GetBlockHash(block Block) string {
blockBytes, _ := json.Marshal(block)
prevHashValueTmp := sha256.Sum256(blockBytes)
return hex.EncodeToString(prevHashValueTmp[:])
}
// verify chain validity
func (b *BlockChain) VerifyChain(chain []Block) bool {
if len(chain) <= 1 {
return true
}
lastBlcok := chain[0]
curChainIdx := 1
for curChainIdx < len(chain) {
curBlcok := chain[curChainIdx]
// verify current block with last block by hash function
if curBlcok.PrevHash != b.GetBlockHash(lastBlcok) {
return false
}
// verify pow of current block
if !b.VerifyPOW(lastBlcok.Pow, curBlcok.Pow) {
return false
}
lastBlcok = curBlcok
curChainIdx++
}
return true
}
type FullChainResp struct {
Chain []Block `json:"chain"`
Len int `json:"len"`
Transactions []Transaction `json:"transactions"`
Nodes []string `json:"nodes"`
}
// resolve conflicts
func (b *BlockChain) ResolveConflicts() {
var maxLenChain []Block
var maxLen = len(b.Chain)
// send request to neighbour nodes, and verify their chain
for _, node := range b.Nodes {
resp, err := http.Get(node + "/fullChain")
if err != nil {
fmt.Println(err)
continue
}
defer resp.Body.Close()
fullChainRespBytes, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Failed to read response body:", err)
return
}
var fullChainResp FullChainResp
err = json.Unmarshal(fullChainRespBytes, &fullChainResp)
if err != nil {
fmt.Println("Failed to read response body:", err)
return
}
if len(fullChainResp.Chain) != fullChainResp.Len {
continue
}
if fullChainResp.Len > maxLen && b.VerifyChain(fullChainResp.Chain) {
maxLen = fullChainResp.Len
maxLenChain = fullChainResp.Chain
}
}
// if find a longer chain, update
if maxLen > len(b.Chain) {
b.Chain = maxLenChain
}
}
// add a new transaction, need to be mined into a new block
func (b *BlockChain) NewTransaction(newTransaction Transaction) {
b.CurrentTransactions = append(b.CurrentTransactions, newTransaction)
}
// get transaction size
func (b *BlockChain) GetCurrentTransactionsSize() int {
return len(b.CurrentTransactions)
}
// move the transaction into a new block
func (b *BlockChain) NewBlock(proof uint64) Block {
prevHash := b.GetBlockHash(b.Chain[len(b.Chain)-1])
block := Block{
Index: len(b.Chain),
TimeStamp: time.Now().Unix(),
Transactions: b.CurrentTransactions,
Pow: proof,
PrevHash: prevHash,
}
b.Chain = append(b.Chain, block)
b.CurrentTransactions = []Transaction{}
return block
}
// verify a prrof of work
func (b BlockChain) VerifyPOW(lastProof uint64, curProof uint64) bool {
lastProofStr := strconv.FormatUint(lastProof, 10)
curProofStr := strconv.FormatUint(curProof, 10)
hashValueTmp := sha256.Sum256([]byte(lastProofStr + curProofStr))
hashValue := hex.EncodeToString(hashValueTmp[:])
if hashValue[0:2] == "00" {
return true
} else {
return false
}
}
// get node identifier
func (b *BlockChain) GetNodeIdentifier() string {
return b.NodeIdentifier
}
// get proof of work, find a number curProof that let make hash( chan[-1].pow + curProof) begin with two zero
func (b *BlockChain) GetPOW() uint64 {
lastProof := b.Chain[len(b.Chain)-1].Pow
var curProof uint64 = 0
for !b.VerifyPOW(lastProof, curProof) {
curProof++
}
return curProof
}
BlcokChain 主要实现了以下几种方法:
- RegisterNode 注册节点方法,向当前的区块链节点注册周边的节点
- GetNodes, 获取周边节点的方法,在一致性检验时用得到
- GetBlockHash, 获取一整个区块的hash值, 在将区块链"串联"起来时用到
- VerifyChain, 区块链校验方法, 从递归区块开始依次向后遍历,根据前一个区块校验后一个区块的prevHash是否有效,根据前一个区块校验后一个区块的 pow 是否有效,如果遍历到最后一个节区块都是有效的,那么整个区块链就是有效的
- ResolveConflicts , 区块链一致性检测方法,当前节点回想周边节点请求周边节点的区块链,若周边节点区块链有效且更长,则以周边节点的区块链为准
- NewTransaction, 新增交易方法,只增加交易,并不打包
- GetCurrentTransactionSize, 返回带打包的交易量
- NewBlock, 打包方法, 对已有的交易进行打包
- VerifyPOW, 工作量证明校验方法, 查看上一个区块的工作量证明与当前工作量证明的 sha256 哈希结果是否以00打头。这里简单起见我只用了两个0,实际的区块链困难需要十几二十个0打头
- GetPOW, 工作量证明方法, 寻找一个随机数 使得 上一个区块的工作量证明与当前工作量证明的 sha256 哈希结果是否以00打头
- GetNodeIdentifier,获取当前区块链的唯一标识
main.go
go
package main
import (
"block_chain_demo/block_chain"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
)
var blockChain *block_chain.BlockChain
func mineHandler(w http.ResponseWriter, r *http.Request) {
// check the transaction size, if it's empty return false
if blockChain.GetCurrentTransactionsSize() == 0 {
w.Write([]byte("current transaction empty"))
return
}
// get proof of work
proof := blockChain.GetPOW()
// update transaction
newTransaction := block_chain.Transaction{
Sender: "0",
Recipient: blockChain.GetNodeIdentifier(),
Amount: 1,
}
blockChain.NewTransaction(newTransaction)
// new a block
block := blockChain.NewBlock(proof)
// return the new block
resp, _ := json.Marshal(block)
w.Write(resp)
return
}
type FullChainResp struct {
Chain []block_chain.Block `json:"chain"`
Len int `json:"len"`
Transactions []block_chain.Transaction `json:"transactions"`
Nodes []string `json:"nodes"`
}
func fullChainHandler(w http.ResponseWriter, r *http.Request) {
resp := &FullChainResp{
Chain: blockChain.Chain,
Len: len(blockChain.Chain),
Transactions: blockChain.CurrentTransactions,
Nodes: blockChain.Nodes,
}
fmt.Println("full chain handler : ", blockChain.Chain)
respBytes, _ := json.Marshal(resp)
w.Write(respBytes)
return
}
// new a transaction into block chain, need to be mined
func newTransactionHandler(w http.ResponseWriter, r *http.Request) {
// parse body
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
fmt.Println("Fail to read request body")
w.Write([]byte("Fail to read request body"))
return
}
var req block_chain.Transaction
err = json.Unmarshal(bodyBytes, &req)
if err != nil {
fmt.Println("Fail to convert request body into transaction Req")
fmt.Println("body = ", string(bodyBytes))
w.Write([]byte("Fail to convert request body into transaction Req"))
return
}
blockChain.NewTransaction(req)
w.Write([]byte(fmt.Sprintf("the transaction will be added to index = %d block", len(blockChain.Chain))))
return
}
type RegisterNodeReq struct {
Node string `json:"node"`
}
// register a neighbour node into the block chain
func registerNodeHandler(w http.ResponseWriter, r *http.Request) {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
fmt.Println("Fail to read request body")
w.Write([]byte("Fail to read request body"))
return
}
var req RegisterNodeReq
err = json.Unmarshal(bodyBytes, &req)
if err != nil {
fmt.Println("Fail to convert request body into transaction Req")
fmt.Println("body = ", string(bodyBytes))
w.Write([]byte("Fail to convert request body into transaction Req"))
return
}
blockChain.RegisterNode(req.Node)
nodes := blockChain.GetNodes()
nodesStr := ""
for i := 0; i < len(nodes); i++ {
nodesStr = nodesStr + nodes[i] + " "
}
w.Write([]byte(nodesStr))
return
}
// resolve conflicts with neighbour nodes
func resolveConflictsHandler(w http.ResponseWriter, r *http.Request) {
blockChain.ResolveConflicts()
chainBytes, _ := json.Marshal(blockChain.Chain)
w.Write(chainBytes)
return
}
func main() {
var nodeIdentifier string
var port string
flag.StringVar(&nodeIdentifier, "ni", "", "node identifier")
flag.StringVar(&port, "p", "", "http port")
flag.Parse()
fmt.Println("nodeIdentifier = ", nodeIdentifier)
fmt.Println("port = ", port)
blockChain = &block_chain.BlockChain{}
blockChain.Init(nodeIdentifier)
http.HandleFunc("/fullChain", fullChainHandler)
http.HandleFunc("/newTransaction", newTransactionHandler)
http.HandleFunc("/mine", mineHandler)
http.HandleFunc("/registerNode", registerNodeHandler)
http.HandleFunc("/resolveConflicts", resolveConflictsHandler)
go func() {
http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
}()
select {}
}
main.go 实现了主函数逻辑,以及区块链对外暴露的一些功能:
- fullChainHandler, 返回区块链的所有信息
- newTransactionHandler, 往区块链中加入校验信息,但并不打包
- mineHandler, 挖矿接口, 先进行工作量证明计算, 然后奖励自己一个虚拟币并加入 区块链交易中,最后调用 newBlcok 方法进行打包
- registerNodeHandler, 向当前节点注册相邻节点的接口
- resolveConflictsHandler, 一致性校验接口,调用 ResolveConflicts 方法进行一致性校验
至此,我们已经实现了一个简单的区块链,下面我们将这个区块链跑起来
测试区块链
运行区块链
首先我们需要将这个 go 项目进行编译, 在main目录下命令行输入
shell
go build
这将在main目录下生成一个可执行文件 main 我们在当前main目录下命令行输入
shell
./main -ni node1 -p 9000
这将会启动一个 名叫 node1 的区块链,并且监听本地 9000端口
将可执行文件复制到另一个目录(随便一个目录,你喜欢就好),令行输入
shell
./main -ni node2 -p 10000
这将会启动一个 名叫 node2 的区块链,并且监听本地 10000端口
测试区块链
测试节点1 fullChain 接口
我们可以看到node1一开始只有一个初始块,这是符合预期的
测试节点2 fullChain 接口
我们可以看到node2一开始只有一个初始块,这也是符合预期的
测试节点1 newTransaction 接口
我们往 node1 区块链中新增了一条交易记录,返回这条交易将会被打入 index = 1 的区块中
我们调用一下fullChain 接口验证一下, 确实被加入了 tansactions 字段中
测试节点1 mine 接口
随后我们对node1 进行挖矿, 成功挖矿后返回打包好的区块
我们调用一下fullChain 接口验证一下, 之前的 transactions 被加入到了新的 block 中, 原有的 transactions 字段被清空了
测试节点2 注册接口
我们在 node2 中注册了一个节点 http://localhost:9000, 其实就是注册了 node1
我们调用一下fullChain 接口验证一下, 确实新增了一个邻近节点
测试节点2 一致性校验接口
我们调用 resolveConflict 进行一致性校验, 可以看到返回了的 chain 里面已经由两个 block了,这个 chain 和 node1 中的 chain 一模一样
我们调用一下fullChain 接口验证一下, chain 确实被更新了,和 node1 中的 chain 保持一致
至此, 我们已经是心啊了一个极简的区块链并完成了测试