到目前为止,我们已经基本实现了区块链中的所有元素。在第一章节就讲到区块链是一个分布式数据库,这也是去中心化的核心所在,但是在前面的章节我们在实现区块链中的各种功能的过程中我们并没有去考虑‘分布式’的问题,只是关注了数据库这部分的实现。在这一章节中我们来讨论区块链的分布式的实现。
本章之后的全部代码已经上传到github上面了,可以点击 这里 查看。
在开始之前我们先总结一下我们已经完成的工作有哪些;
区块链的基本原型
在上一章中,我们没有实现挖矿奖励,我们只有在创建区块链的时候coinbaseTX给了奖励,但是之后每一次挖矿都没有给出奖励。所以我们要实现每一个区块被挖出后要给矿工一笔挖矿奖励的交易,挖矿奖励实际上就是一笔CoinbaseTX,coinbase交易只有一个输出,我们实现挖矿奖励非常简单,coinbase交易放在区块的Transactions的第一个位置就行了。
修改send方法,实现挖矿奖励:
//send方法
func (cli *CLI) send(from,to string,amount int) {
if !wallet.ValidateAddress(from) {
log.Panic("ERROR: Address is not valid")
}
if !wallet.ValidateAddress(to) {
log.Panic("ERROR: Address is not valid")
}
bc := blockchain.NewBlockchain(from)
defer bc.Db().Close()
tx := blockchain.NewUTXOTransaction(from,to,amount,bc)
//挖矿奖励的交易,把挖矿的奖励发送给矿工,这里的矿工默认为发送交易的地址
cbtx := transaction.NewCoinbaseTX(from,"")
//挖出一个包含该交易的区块,此时区块还包含了-挖矿奖励的交易
bc.MineBlock([]*transaction.Transaction{cbtx,tx})
fmt.Println("发送成功...")
}
下面将实现UTXO集:
在第三章我们实现把区块链存储在数据库的时候,我们创建了一个专门存储区块链的数据库blockchain.db,这样的好处是使我们链上的区块信息写入我们电脑上的磁盘上,实现永久保存。众所周知,区块相当于一个分布式账本,只要安装了相同区块链的终端节点,都可以看到这个区块链账本上的信息,这里的账本信息就是数据库blockchain.db上的信息。如果我们安装的全节点的话,我们就会去下载区块链上的所有区块信息,也就是这条链的blockchain.db,可以理解为我们所说的分布式账本中的账本就是blockchain.db,分布式就是我们所有节点都可以按照规定的规则对这个账本进行操作,所有拥有这个账本的节点都有相同的权限。
好了,我们大致了解了区块链中的blockchain.db之后,我们可能会有疑问,大家一想到数据库就会条件反射的想到是会存放很多数据。没错,当我们这条链越来越长的时候,我们的blockchain.db也就会越来越大,这时候实现区块链的分布式就没那么容易了,因为当这个数据库很大很大的时候,比如现在比特币的这个数据库已经超过100Gb了,一般用户想运行全节点用电脑想下载下来都比较困难。怎么办喃,UTXO集就是来解决这个问题的。
在我们的区块链中我们会创建另外一个blot数据库,叫做chainstate.db。顾名思义链状态数据库,存放区块链状态的数据库。区块链状态? 没错这里的区块链状态其实就是区块链中所有的未花费交易输出,我们就把这个表示为UTXO集。说明一点我们创建的chainstate.db值存放的是未花费交易输出,并不是存放完整的交易信息,所以比起blockchain.db小太多了。
我们为什么要UTXO集喃?首先从代码层面来讲我们我们有这个需求就是找到当前区块链中的所以未花费交易和未花费交易输出。比如下面这三个方法:
//在区块链上找到每一个区块中属于address用户的未花费交易输出,返回未花费输出的交易切片
func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []transaction.Transaction {
//通过找到未花费输出交易的集合,我们返回集合中的所有未花费的交易输出
func (bc *Blockchain) FindUTXO(pubKeyHash []byte) []transaction.TXOutput {
//找到可以花费的交易输出,这是基于上面的FindUnspentTransactions 方法
func (bc *Blockchain) FindSpendableOutputs(pubKeyHash []byte,amount int) (int,map[string][]int) {
无一例外,我们实现以上方法都是需要遍历整个区块链,区块链的长度短还好办,但是像比特币这样的区块链,我们这样一遍又一遍的去遍历它的链,可能你电脑的cpu要发疯,想想都难受。 解决之道就是用chainstate数据库存放了区块链中所有的未花费交易,这样我们需要区块链中的UTXO的时候,就只需要在chainstate.db中找就好了。这就是UTXO集要做的事情:这是一个从所有区块链交易中构建(对区块进行迭代,但是只须做一次)而来的缓存,然后用它来计算余额和验证新的交易。目前为止,比特币区块链中的UTXO 集大概有 3Gb,这就对我们一般不需要运行全节点挖矿的用户比较友好了。
现在我们重新创建一个utxo包,用于实现UTXO集,因为我们这种行为是对区块链的操作,所以我们肯定是要绑定区块链的,下面创建一个UTXOSet结构体
然后我们要初始化UTXO集,这个初始化工作在代码中只运行一次,得到的结果是一个存放UTXO集的数据库,代码如下:
//构建UTXO集的索引并存储在数据库的bucket中
func (u UTXOSet) Reindex() {
//调用区块链中的数据库,这里的Db()是格式工厂Blockchain结构体中的字段db
db := u.Blockchain.Db()
//桶名
bucketName := []byte(utxoBucket)
//对数据库进行读写操作
err := db.Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket(bucketName) //因为我们是要哦重新建一个桶,所以如果原来的数据库中有相同名字的桶,则删除
if err != nil && err != bolt.ErrBucketNotFound {
log.Panic(err)
}
//创建新桶
_,err = tx.CreateBucket(bucketName)
if err != nil {
log.Panic(err)
}
return nil
})
if err != nil {
log.Panic(err)
}
//返回链上所有未花费交易中的交易输出
UTXO := u.Blockchain.FindUTXO()
//把未花费交易中的交易输出集合写入桶中
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketName)
//写入键值对
for txID,outs := range UTXO {
key,err := hex.DecodeString(txID)
if err != nil {
log.Panic(err)
}
err = b.Put(key,outs.Serialize())
if err != nil {
log.Panic(err)
}
}
return nil
})
}
我们如果要发送币给别人就要在查找本次发送回用到哪些未花费的交易输出:
//查询并返回被用于这次花费的输出,找到的输出的总额要刚好大于要花费的输入额
func (u UTXOSet) FindSpendableOutputs(pubkeyHash []byte,amount int) (int,map[string][]int) {
//存储找到的未花费输出集合
unspentOutputs := make(map[string][]int)
//记录找到的未花费输出中累加的值
accumulated := 0
db := u.Blockchain.Db()
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(utxoBucket))
//声明一个游标,类似于我们之前构造的迭代器
c := b.Cursor()
//用游标来遍历这个桶里的数据,这个桶里装的是链上所有的未花费输出集合
for k,v := c.First(); k != nil; k,v =c.Next() {
txID := hex.EncodeToString(k)
outs := transaction.DeserializeOutputs(v)
for outIdx,out := range outs.Outputs {
if out.IsLockedWithKey(pubkeyHash) && accumulated < amount {
accumulated += out.Value
unspentOutputs[txID] = append(unspentOutputs[txID],outIdx)
}
}
}
return nil
})
if err != nil {
log.Panic(err)
}
return accumulated,unspentOutputs
}
检查地址所属的未花费输出,用于查询余额:
//查询对应的地址的未花费输出
func (u UTXOSet) FindUTXO(pubKeyHash []byte) []transaction.TXOutput {
var UTXOs []transaction.TXOutput
db := u.Blockchain.Db()
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(utxoBucket))
c := b.Cursor()
for k,v := c.First();k != nil;k,v = c.Next() {
outs := transaction.DeserializeOutputs(v)
for _,out := range outs.Outputs {
if out.IsLockedWithKey(pubKeyHash) {
UTXOs = append(UTXOs,out)
}
}
}
return nil
})
if err != nil {
log.Panic(err)
}
return UTXOs
}
更新UTXO集,每增加一个区块就要去更新:
//当区块链中的区块增加后,要同步更新UTXO集,这里引入的区块为新加入的区块。
func (u UTXOSet) Update(block *block.Block) {
db := u.Blockchain.Db()
err := db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(utxoBucket))
for _,tx := range block.Transactions {
if tx.IsCoinbase() == false {
for _,vin := range tx.Vin {
//实例化结构体TXOutputs
updatedOuts := transaction.TXOutputs{}
outsBytes := b.Get(vin.Txid)
outs := transaction.DeserializeOutputs(outsBytes)
for outIdx,out := range outs.Outputs {
if outIdx != vin.Vout {
updatedOuts.Outputs = append(updatedOuts.Outputs,out)
}
}
if len(updatedOuts.Outputs) == 0 {
err := b.Delete(vin.Txid)
if err != nil {
log.Panic(err)
}
}else{
err := b.Put(vin.Txid,updatedOuts.Serialize())
if err != nil {
log.Panic(err)
}
}
}
}
newOutputs := transaction.TXOutputs{}
for _,out := range tx.Vout {
newOutputs.Outputs = append(newOutputs.Outputs,out)
}
err := b.Put(tx.ID,newOutputs.Serialize())
if err != nil {
log.Panic(err)
}
}
return nil
})
if err != nil {
log.Panic(err)
}
}
下面是用于返回UTXO集中的交易总数:
//返回UTXO集中的交易数
func (u UTXOSet) CountTransactions() int {
db := u.Blockchain.Db()
counter := 0
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(utxoBucket))
c := b.Cursor()
for k,_ := c.First(); k != nil; k,_ = c.Next() {
counter++
}
return nil
})
if err != nil {
log.Panic(err)
}
return counter
}
使用UTXO集主要是有两个地方:
还有其他很多地方也会做出改动,这里就不一一叙述了,实在是太多地方要改动了。下面测试一下运行结果吧。
D:\Program Files\goproject\src\go_code\A_golang_blockchain\main>main.exe createblockchain -address 12pRSDVKiiUJnZAmgC5JedstusZXdYbLsx
工作量证明成功 hash= 00131d6e788d555384aff87dd863524b5e1df6c3412db27d22381347af19d8e6 nonce = 664
Done!
D:\Program Files\goproject\src\go_code\A_golang_blockchain\main>main.exe getbalance -address 12pRSDVKiiUJnZAmgC5JedstusZXdYbLsx
Balance of '12pRSDVKiiUJnZAmgC5JedstusZXdYbLsx':50
D:\Program Files\goproject\src\go_code\A_golang_blockchain\main>main.exe
Usage:
createblockchain -address ADDRESS 创建一条链并且该地址会得到狗头金
createwallet - 创建一个钱包,里面放着一对秘钥
getbalance -address ADDRESS 得到该地址的余额
listaddresses - Lists all addresses from the wallet file
printchain - 打印链
reindexutxo - Rebuilds the UTXO set
send -from FROM -to TO -amount AMOUNT 地址from发送amount的币给地址to
D:\Program Files\goproject\src\go_code\A_golang_blockchain\main>main.exe send -from 12pRSDVKiiUJnZAmgC5JedstusZXdYbLsx -to 1Lp1WeAXqUhBxNnbe5JFbCjfMUTn2rLPBw -amount 22
工作量证明成功 hash= 00332806d892e7100062fd8abb1134a78ddc0b2ac25eb79c0edf96e35b48134c nonce = 984
发送成功...
D:\Program Files\goproject\src\go_code\A_golang_blockchain\main>main.exe send -from 12pRSDVKiiUJnZAmgC5JedstusZXdYbLsx -to 1Lp1WeAXqUhBxNnbe5JFbCjfMUTn2rLPBw -amount 1
工作量证明成功 hash= 002ee27e512cd1d452639b31afb355ae67a2882d6dd025bae6959367188e029f nonce = 644
发送成功...
D:\Program Files\goproject\src\go_code\A_golang_blockchain\main>main.exe send -from 12pRSDVKiiUJnZAmgC5JedstusZXdYbLsx -to 1Lp1WeAXqUhBxNnbe5JFbCjfMUTn2rLPBw -amount 33
工作量证明成功 hash= 000bade5334037607aaf6623e886e4d11ffe67047127a2b417adf24c969a7822 nonce = 603
发送成功...
D:\Program Files\goproject\src\go_code\A_golang_blockchain\main>main.exe printchain
------======= 区块 000bade5334037607aaf6623e886e4d11ffe67047127a2b417adf24c969a7822 ============
时间戳:1535457293
PrevHash:002ee27e512cd1d452639b31afb355ae67a2882d6dd025bae6959367188e029f
POW is true
--Transaction d13393356e9faba0eaffefbb659c5d9057f02308b870522fc3b034780411f21b:
-Input 0:
TXID:
Out: -1
Signature:
PubKey:e5a596e58ab1e7bb992027313270525344564b6969554a6e5a416d6743354a6564737475735a586459624c737827
-Output 0:
Value: 50
Script: 13f012b5d9f91cdfd0f743f0267ec4a5df9cc15f
--Transaction 70d937f9e600ee5b647234a4b5acf37a6a408409e79a38504dabf6ab3cef3042:
-Input 0:
TXID: 35150b8d5cc645e1bcce9581e6becb788dcf06e8d06801019517e88a1bca8af1
Out: 1
Signature: 41bb6d16c20d454b6ad5749bf639dd0a4ca8a41e7c4565a05a19b31c824b2243369ba1b7f1c9dcfd8449c7b9ecb1cb87125b73a63a878534e511179e4c7967f8
PubKey:73060d7503440b2195505875209ed717dc3c08e66710d4dfea6c76ff1f39537b696da053ef47a008243fb05158b650270670e2da09e9b77cb389ebf9f874f8a7
-Input 1:
TXID: d13393356e9faba0eaffefbb659c5d9057f02308b870522fc3b034780411f21b
Out: 0
Signature: 14198cdcf02e15953b93a5a4c006d62f96923e63cdc5baeb4017c2822db010431fdfdf0472315c4ca01c2ebf3fe0202c9fb5d524fafaf948c28348de7d80aec5
PubKey:73060d7503440b2195505875209ed717dc3c08e66710d4dfea6c76ff1f39537b696da053ef47a008243fb05158b650270670e2da09e9b77cb389ebf9f874f8a7
-Output 0:
Value: 33
Script: d94e5ddfda60067c3a566a887eae0d0bd32ef2e8
-Output 1:
Value: 44
Script: 13f012b5d9f91cdfd0f743f0267ec4a5df9cc15f
------======= 区块 002ee27e512cd1d452639b31afb355ae67a2882d6dd025bae6959367188e029f ============
时间戳:1535457289
PrevHash:00332806d892e7100062fd8abb1134a78ddc0b2ac25eb79c0edf96e35b48134c
POW is true
--Transaction d13393356e9faba0eaffefbb659c5d9057f02308b870522fc3b034780411f21b:
-Input 0:
TXID:
Out: -1
Signature:
PubKey:e5a596e58ab1e7bb992027313270525344564b6969554a6e5a416d6743354a6564737475735a586459624c737827
-Output 0:
Value: 50
Script: 13f012b5d9f91cdfd0f743f0267ec4a5df9cc15f
--Transaction 35150b8d5cc645e1bcce9581e6becb788dcf06e8d06801019517e88a1bca8af1:
-Input 0:
TXID: 3559dcb2b74cce8afef0ef6e68bf13feef8c040d8bf623e4fe4ea2741b193483
Out: 1
Signature: 2daf9792704831c68b96ea9cca9dd13e8088fd78dbdd72a2e45a4d8f382e316a5c6b6595fbcc766e4e45e55813c17ef0e78cc57f57b5c1d64fe75f90d46f352f
PubKey:73060d7503440b2195505875209ed717dc3c08e66710d4dfea6c76ff1f39537b696da053ef47a008243fb05158b650270670e2da09e9b77cb389ebf9f874f8a7
-Output 0:
Value: 1
Script: d94e5ddfda60067c3a566a887eae0d0bd32ef2e8
-Output 1:
Value: 27
Script: 13f012b5d9f91cdfd0f743f0267ec4a5df9cc15f
------======= 区块 00332806d892e7100062fd8abb1134a78ddc0b2ac25eb79c0edf96e35b48134c ============
时间戳:1535457266
PrevHash:00131d6e788d555384aff87dd863524b5e1df6c3412db27d22381347af19d8e6
POW is true
--Transaction d13393356e9faba0eaffefbb659c5d9057f02308b870522fc3b034780411f21b:
-Input 0:
TXID:
Out: -1
Signature:
PubKey:e5a596e58ab1e7bb992027313270525344564b6969554a6e5a416d6743354a6564737475735a586459624c737827
-Output 0:
Value: 50
Script: 13f012b5d9f91cdfd0f743f0267ec4a5df9cc15f
--Transaction 3559dcb2b74cce8afef0ef6e68bf13feef8c040d8bf623e4fe4ea2741b193483:
-Input 0:
TXID: bcac60e7d91b31a0af5e997057c471900be89d83ab293efd3e394e433183f516
Out: 0
Signature: f0611dd2072baad549188e6c87a136457254f8b1ecffe23de2e974de28cce54f0429ae39f88f4709e967a5569e7668401265312fa32d49fddec3c50c8a45281c
PubKey:73060d7503440b2195505875209ed717dc3c08e66710d4dfea6c76ff1f39537b696da053ef47a008243fb05158b650270670e2da09e9b77cb389ebf9f874f8a7
-Output 0:
Value: 22
Script: d94e5ddfda60067c3a566a887eae0d0bd32ef2e8
-Output 1:
Value: 28
Script: 13f012b5d9f91cdfd0f743f0267ec4a5df9cc15f
------======= 区块 00131d6e788d555384aff87dd863524b5e1df6c3412db27d22381347af19d8e6 ============
时间戳:1535457141
PrevHash:
POW is true
--Transaction bcac60e7d91b31a0af5e997057c471900be89d83ab293efd3e394e433183f516:
-Input 0:
TXID:
Out: -1
Signature:
PubKey:5468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73
-Output 0:
Value: 50
Script: 13f012b5d9f91cdfd0f743f0267ec4a5df9cc15f
D:\Program Files\goproject\src\go_code\A_golang_blockchain\main>main.exe reindexutxo
Done! There are 4 transactions in the UTXO set.
从上面结果来看,我们先创建了一条新链,然后连续发送了3笔交易,打印链出来确实是这样的,一共有4个区块,第一个区块是创世区块。除开第一个创世区块,后面的每一个区块都包含了2笔交易记录,这是第一笔交易是给矿工的奖励交易,每个区块都会有的。后面的交易中会发现交易输入都只有一个,输出往往是两个,因为这是找零的操作,发送方把发送给接收方之后剩余的币发送给自己地址了。最后我们调用了查找UTXO集中的交易数,结果是4,完全是我们的预期结果。
下面我们将实现Merkle树,首先需要讲的是什么是默克尔树,比特币上使用了默克尔树。简单讲默克尔树是为了实现简易支付验证(Simplified Payment Verification, SPV)。就是我们常说的轻钱包就是用默克尔树来实现的。它不需要下载整个区块链,也不需要验证区块和交易。相反,它会在区块链查找交易(为了验证支付),并且需要连接到一个全节点来检索必要的数据。这个机制允许在仅运行一个全节点的情况下有多个轻钱包。
为了实现 SPV,需要有一个方式来检查是否一个区块包含了某笔交易,而无须下载整个区块。这就是 Merkle 树所要完成的事情。
比特币用 Merkle 树来获取交易哈希,哈希被保存在区块头中,并会用于工作量证明系统。到目前为止,我们只是将一个块里面的每笔交易哈希连接了起来,将在上面应用了 SHA-256 算法。虽然这是一个用于获取区块交易唯一表示的一个不错的途径,但是它没有利用到 Merkle 树。
来看一下 Merkle
这个图很形象的描述了默克尔树的构造。每个块都会有一个 Merkle 树,它从叶子节点(树的底部)开始,一个叶子节点就是一个交易哈希(比特币使用双 SHA256 哈希)。叶子节点的数量必须是双数,但是并非每个块都包含了双数的交易。因为,如果一个块里面的交易数为单数,那么就将最后一个叶子节点(也就是 Merkle 树的最后一个交易,不是区块的最后一笔交易)复制一份凑成双数。从下往上,两两成对,连接两个节点哈希,将组合哈希作为新的哈希。新的哈希就成为新的树节点。重复该过程,直到仅有一个节点,也就是树根。根哈希然后就会当做是整个块交易的唯一标示,将它保存到区块头,然后用于工作量证明。Merkle 树的好处就是一个节点可以在不下载整个块的情况下,验证是否包含某笔交易。并且这些只需要一个交易哈希,一个 Merkle 树根哈希和一个 Merkle 路径。
代码实现:
package merkle_tree
import (
"crypto/sha256"
)
//创建结构体
type MerkleTree struct {
RootNode *MerkleNode
}
type MerkleNode struct {
Left *MerkleNode
Right *MerkleNode
Data []byte
}
//创建一个新的节点
func NewMerkleNode(left,right *MerkleNode,data []byte) *MerkleNode {
mNode := MerkleNode{}
if left == nil && right == nil {
//叶子节点
hash := sha256.Sum256(data)
mNode.Data = hash[:]
} else {
prevHashes := append(left.Data,right.Data...)
hash := sha256.Sum256(prevHashes)
mNode.Data = hash[:]
}
mNode.Left = left
mNode.Right = right
return &mNode
}
//生成一颗新树
func NewMerkleTree(data [][]byte) *MerkleTree {
var nodes []MerkleNode
//输入的交易个数如果是单数的话,就复制最后一个,成为复数
if len(data) % 2 != 0 {
data = append(data,data[len(data) - 1])
}
//通过数据生成叶子节点
for _,datum := range data {
node := NewMerkleNode(nil,nil,datum)
nodes = append(nodes,*node)
}
//循环一层一层的生成节点,知道到最上面的根节点为止
for i := 0; i < len(data)/2; i++ {
var newLevel []MerkleNode
for j := 0; j < len(nodes); j += 2 {
node := NewMerkleNode(&nodes[j],&nodes[j+1],nil)
newLevel = append(newLevel,*node)
}
nodes = newLevel
}
mTree := MerkleTree{&nodes[0]}
return &mTree
}
之前我们是把所有交易进行哈希,现在我们需要通过修改交易哈希函数即可:
func (b *Block) HashTransactions() []byte {
//var txHash [32]byte
//var txHashes [][]byte
var transactions [][]byte
for _,tx := range b.Transactions {
//txHashes = append(txHashes,tx.Hash())
transactions = append(transactions,tx.Serialize())
}
//txHash = sha256.Sum256(bytes.Join(txHashes,[]byte{}))
mTree := merkle_tree.NewMerkleTree(transactions)
//return txHash[:]
return mTree.RootNode.Data
}
这一章代码量有点大,我这里就不贴在下面了,现在还没有完成这个项目,等完全写完了我会发个链接在这里的。如果现在需要这章节代码的请联系我吧。
写在最后,我们已经实现了一个基于区块链的加密货币的几乎所有关键特性。我们已经有了区块链,地址,挖矿和交易。但是要想给这些所有的机制赋予生命,让比特币成为一个全球系统,还有一个不可或缺的环节:共识(consensus)。在下一篇文章中,我们将会开始实现区块链的“去中心化(decenteralized)”。
本人微信公众号,欢迎加入