用go撸一个简易版的区块链

引言

这个最初的版本时多年以前学习go的时候,自己撸的一个简易版本的区块链。不过麻雀虽小,五脏俱全。通过这个代码你了解区块链内部的大概运行机制时没有问题的。

比特币底层区块链的代码非常复杂,但是我们可以从中梳理几个核心的概念,然后对应进行简单的实现。通过这些简易版本的实现我们可以以小窥大。下面我们先来梳理下几个核心的概念。

交易

拿比特币举例,A给B转账,这是一笔交易。更广义的概念,交易可以形容数据库网络中发生的每一次改变,可以是一笔转账、一个事件通知、或一段信息。交易中通常包含发送者的信息,接收者的信息,交易金额等信息。

这是交易的概念。

区块

区块链 是一个共享的、不可篡改的账本,旨在促进业务网络中的交易记录和资产跟踪流程。拿比特币交易举例,比如A向B转账,这笔交易会在区块链公网进行广播,并在各节点间共享这一信息。每十分钟左右,挖矿者会将这些交易收集到一个新区块中。

这是区块的概念。

把一个个验证后的合法的区块连在一起,形成的就是链。

哈希(hash)

哈希函数(Hash Function),也称为散列函数,给定一个输入x,它会算出相应的输出H(x)。只要输入变化,输出的哈希结果必然也会变化。

区块里面除了包含交易的信息之外,还会将一个加密哈希附加到新区块的最后。如果修改了区块的内容,则该哈希值也将更改,这将提供一种检测数据篡改的方式。

这是哈希的概念。

工作量证明

就是我们俗称的挖矿。矿工(就是区块链节点)创建区块,如果想把区块加入区块链,矿工需要不断的在区块中加入一个随机数并计算一个哈希值,只有这个值小于的某个目标值才能加入链中。这个目标值决定了挖矿的难度。

源码解析

有了上面的知识储备后,我们来分析下源码。

注意我只会贴出来关键的代码,会把不影响核心逻辑的都去掉。 全部的源码再文章最后的链接给出。

整个工程的目录结构时这样的,

pkg里面存放的是核心业务代码,也就是区块链的核心逻辑。同时也包括对应的单元测试程序。cmd目录下存放的是程序的运行入口,也就是我们的main函数。

我们从入口开始,采用从全局到细节的流程来剖析源码。

func main() {port := flag.String("port", "8001", "use -port ")flag.Parse()fmt.Printf("port is:%s\n", *port)http.HandleFunc("/mine", MineHandler)http.HandleFunc("/transactions/new", NewTransactionHandler)http.HandleFunc("/nodes/register", RegisterNodesHandler)http.HandleFunc("/nodes/resolve", ConsensusHandler)http.HandleFunc("/chain", ChainHandler)http.ListenAndServe(fmt.Sprintf(":%s", *port), nil)}

main函数没有业务逻辑,就是通过http的方式给我们提供了测试的入口,让我们可以通过http请求(get或者post)发起对应的功能。

首先我们来看看,注册节点的方法RegisterNodesHandler,所谓的节点就是矿工的意思。我们可以通过命令行的方式(./goblockchain -port 8001)来启动一个节点,同时通过RegisterNodesHandler把其他节点加入进来。

func RegisterNodesHandler(w http.ResponseWriter, req *http.Request) {type Nodes_ST struct {Nodes []string}type ResponseJsonBean struct {Message string `json:"message"`Data[]string `json:"total_nodes"`}nodeGroup := Nodes_ST{}result := ResponseJsonBean{}req.ParseForm()b, _ := ioutil.ReadAll(req.Body)if req.Method == "POST" {json.Unmarshal([]byte(b), &nodeGroup)result.Data = make([]string, 0)for _, node := range nodeGroup.Nodes {fmt.Printf("node:%s\n", node)goblockchain.Register_Node(node)result.Data = append(result.Data, node)}code = http.StatusCreatedresult.Message = "New nodes have been added"}bytes, _ := json.Marshal(result)w.WriteHeader(code)fmt.Fprintf(w, string(bytes))}

这部分的逻辑就是解析post请求的节点数,然后循环调用(循环次数是节点的数量)调用Register_Node方法注册节点,请求的数据类型下面这样的格式:

{"nodes": ["http://127.0.0.1:8002","http://127.0.0.1:8003"]}

继续来看下Register_Node方法,

func (bc *Blockchain) Register_Node(address string) {u, err := url.Parse(address)bc.nodes = append(bc.nodes, u.Host)}

其实就是加入了一个blockchain实例的nodes元素的数组里,nodes的定义如下:

type Blockchain struct {current_transactions []Transactionchain[]blocknodes[]string}

然后我们看看在某个节点上创建一笔交易的流程。入口函数是NewTransactionHandler

func NewTransactionHandler(w http.ResponseWriter, req *http.Request) {//交易的元数据type Transaction_ST struct {Senderstring `json:sender`Recipient string `json:recipient`Amountint`json:amount`}type ResponseJsonBean struct {Message string `json:"message"`}transaction := Transaction_ST{}result := ResponseJsonBean{}req.ParseForm()b, _ := ioutil.ReadAll(req.Body)if req.Method == "POST" {json.Unmarshal([]byte(b), &transaction)index := goblockchain.New_Transaction(transaction.Sender, transaction.Recipient, transaction.Amount)code = http.StatusCreatedresult.Message = fmt.Sprintf("New nodes have been added to Block %d", index)}bytes, _ := json.Marshal(result)w.WriteHeader(code)fmt.Fprintf(w, string(bytes))}

解析post提交的交易数据,然后请求New_Transaction方法创建交易。请求的数据示例如下:

{"sender": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa","recipient": "1Ez69SnzzmePmZX3WpEzMKTrcBF2gpNQ55","amount": 1000}

New_Transaction方法也比较简单,

func (bc *Blockchain) New_Transaction(sender string, recipient string, amount int) int {var trans Transactiontrans.Sender = sendertrans.Recipient = recipienttrans.Amount = amountbc.current_transactions = append(bc.current_transactions, trans)block := bc.Last_block()return block.Index + 1}

创建交易,放到blockchain实例的交易数组里,这里有个Last_block方法也注意下,它的实现如下:

func (bc *Blockchain) Last_block() block {height := len(bc.chain)block := bc.chain[height-1]return block}

这个方法就是返回了当前区块链的最后一个区块的实例。

然后,就可以挖矿了,哈哈。入口函数是MineHandler

func MineHandler(w http.ResponseWriter, req *http.Request) {//区块的数据结构type Block_ST struct {Index int `json:index`Message string`json:message`Transactions[]Transaction `json:transactions`Proof int `json:proof`Previous_hash string`json:previous_hash`}result := Block_ST{}req.ParseForm()last_block := goblockchain.Last_block()last_proof := last_block.Proofprevious_hash := last_block.Previous_hash//开始挖坑proof := goblockchain.Proof_of_work(last_proof)var trans_reward Transactiontrans_reward.Sender = "0"trans_reward.Recipient = "random address"trans_reward.Amount = 1//挖坑成功,形成新的区块并加入链block := goblockchain.New_Block(proof, previous_hash)code = http.StatusOKresult.Message = "New Block Forged"result.Index = block.Indexresult.Proof = block.Proofresult.Transactions = block.Transactionsresult.Previous_hash = block.Previous_hashbytes, _ := json.Marshal(result)w.WriteHeader(code)fmt.Fprintf(w, string(bytes))}

这个代码稍长,不过也很好理解。首先是获取链上最后一个区块的证明值(last_proof),之所以要获取它是因为计算新区块的哈希需要使用上一个区块的证明值。这样能保证整个链上的区块都是互相关联的。

工作量证明的方法是Proof_of_work

func (bc *Blockchain) Proof_of_work(last_proof int) int {proof := 0for !(valid_proof(last_proof, proof)) {proof++}return proof}

这里的proof就是新区块一直在尝试的随机值,然后用valid_proof验证是否满足条件(也就是挖矿是否成功),源码如下:

func valid_proof(last_proof int, proof int) bool {str_last_proorf := []byte(strconv.FormatInt(int64(last_proof), 10))str_proof := []byte(strconv.FormatInt(int64(proof), 10))str_data := bytes.Join([][]byte{str_last_proorf, str_proof}, []byte{})guess_hash := sha256.Sum256(str_data)return bytes.Equal(guess_hash[:2], []byte("00"))}

这里可以看到,计算哈希的时候需要把链上最后一个区块的证明值作为其中一个输入。

挖矿成功后,就可以形成新的区块放入链上了,方法是New_Block,源码如下:

func (bc *Blockchain) New_Block(proof int, previous_hash string) *block {blockinstance := &block{}blockinstance.Index = len(bc.chain) + 1blockinstance.timestamp = time.Now().Unix()blockinstance.Transactions = bc.current_transactionsblockinstance.Proof = proofblockinstance.Previous_hash = previous_hashtrans_len := len(bc.current_transactions)fmt.Printf("trans_len:%d \n", trans_len)fmt.Printf("index:%d \n", blockinstance.Index)bc.current_transactions = bc.current_transactions[trans_len:] //clearbc.chain = append(bc.chain, *blockinstance)return blockinstance}

这里的逻辑也很简单,生成一个新的区块实例,加入交易的信息,工作量证明值,上一个区块的哈希,然后链接在区块链的最后即可。

测试

首先编译下源码,进入cmd目录,执行:

$ go build -o goblockchain

然后开启三个命令行窗口,分别执行下面的命令,启动三个节点(矿工)

./goblockchain -port 8001./goblockchain -port 8002./goblockchain -port 8003

然后我们在node1执行添加节点的命令,

可以看到执行成功了。

接着我们创建一笔交易,

可以看到创建成功了。

然后执行挖矿

可以看到挖矿成功了。

然后我们可以查看下当前区块链的信息:

curl 127.0.0.1:8001/chain

返回的结果是:

{Index:1 timestamp:1655738834 Transactions:[] Proof:1 Previous_hash:1} --> {Index:2 timestamp:1655738903 Transactions:[{Sender:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa Recipient:1Ez69SnzzmePmZX3WpEzMKTrcBF2gpNQ55 Amount:1000}] Proof:9561 Previous_hash:1} --> 

可以看到是有两个区块,第一个是默认生成的不包含交易信息,第二个区块就是我们自己挖的。


源码地址如下:

https://github.com/pony-maggie/blockchain