用go编写区块链系列之6--交易2
0 概述
在这个系列的前面几篇文章我们说区块链是一个分布式数据库,但是在实践中,我们选择忽略了“分布式”而只关注“数据库”。目前为止我们我们实现了区块链作为数据库的所有特性。这篇文章中我们将实现前面几篇中我们忽略的一些机制,下篇文章我们将实现分布式特性。
1 挖矿奖励
我们忽略的一件事情是挖矿奖励。挖矿奖励是一个coinbase交易。当一个矿工开始挖掘新区快时,它将队列中的新交易打包起来,然后再附加一个coinbase交易。coinbase交易没有输入,只有一个包含矿工公钥哈希的输出。我们实现挖矿奖励只要这样更新send函数:
func (cli *CLI) send(from, to string, amount int) {
...
bc := NewBlockchain()
UTXOSet := UTXOSet{bc}
defer bc.db.Close()
tx := NewUTXOTransaction(from, to, amount, &UTXOSet)
cbTx := NewCoinbaseTX(from, "")
txs := []*Transaction{cbTx, tx}
newBlock := bc.MineBlock(txs)
fmt.Println("Success!")
}
在我们的这个实现方法中,提交交易的节点同时也是矿工,所以也会得到奖励。
2 UTXO集合
在比特币中区块和交易输出是分开存储的,区块是被存储在blocks数据库中,交易输出被存储在chainstate数据库中。chainstate数据存储结构为:
1 ‘’c'+32字节的交易哈希 --> 该交易中包含的未花费交易输出
2 'B' --> 32字节的区块哈希
chainstate中不存储交易,它只存储UTXO集合。但是为什么我们需要存储UTXO集合呢?
看一下我们之前实现的函数Blockchain.FindUnspentTransactions:
func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []Transaction {
...
bci := bc.Iterator()
for {
block := bci.Next()
for _, tx := range block.Transactions {
...
}
if len(block.PrevBlockHash) == 0 {
break
}
}
...
}
这个函数要找出所有UTXO。由于交易都是存储在区块中,所以它遍历每一个区块,在一个区块中又遍历每一笔交易。截止到2017年9月18日,比特币中共有485860个区块,整个区块链数据库占用了超过140G字节的空间。如果每次查询都要这样做将是一个很笨重的操作。
解决这个问题的方法是在一个数据表中存储所有的UTXO。这个UTXO数据库从所有的交易中创建,但是它只创建了一次。以后可以用这个数据库执行查询和交易验证之类的工作。截止到2017年9月比特币的UTXO数据库只有2.7G大小。
让我们想一下为了实现UTXO数据集我们需要做哪些修改。目前为止我们使用了这些方法去进行交易查找:
1. Blockchain.FindUnspentTransactions---查找所有未花费的交易输出,就是这个函数遍历了每一个区块。
2. Blockchain.FindSpendableOutputs---当创建新交易的时候,这个函数查找到足额的交易输出来完成这笔交易。
3. Blockchain.FindUTXO---给定公钥哈希查找对应的UTXO,用于查找余额。
4. Blockchain.FindTransaction---在区块链中根据交易哈希查找交易,它遍历了所有区块。
所有这些方法都迭代了区块链的所有区块,我们使用UTXO集可以改进它们中 关于UTXO部分的函数,对于第4个函数则不起作用。我们需要这些方法:
1. Blockchain.FindUTXO---遍历所有区块查找所有的UTXO。
2. UTXOSet.Reindex---使用FindUTXO查找所有UTXO并保存在数据库,这是一种缓存机制。
3. UTXOSet.FindSpendableOutputs---功能类似于Blockchain.FindSpendableOutputs,但是使用了UTXO集。
4. UTXOSet.FindUTXO---功能类似于Blockchain.FindUTXO,但是使用了UTXO集。
5. Blockchain.FindTransaction---保持不变。
开始编码吧。
type UTXOSet struct {
Blockchain *Blockchain
}
首先定义UTXOSet结构,它持有一个区块链对象。
func (u UTXOSet) Reindex() {
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
})
}
这个方法首先在数据库中创建了一个utxoBucket,然后调用BlockChain.FindUTXO从区块链中查找所有的UTXO,然后将它们保存在bucket中。
取得了UTXO集后,现在可以用它来发送金币了:
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 := 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) []TXOutput {
var UTXOs []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 := 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集后,我们的数据是分开存储了。区块数据存储在blockchain数据库里,UTXO数据存储在UTXO集里。这需要这俩个数据库之间的同步。当有新区快到来时,我们就更新UTXO集。
func (u UTXOSet) Update(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 {
updatedOuts := TXOutputs{}
outsBytes := b.Get(vin.Txid)
outs := 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 := 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数据将从数据库中删除。
现在我们可以应用UTXO集了。在创建区块链时先索引一遍:
func (cli *CLI) createBlockchain(address string) {
bc := CreateBlockchain(address)
defer bc.db.Close()
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()
fmt.Println("Done!")
}
当产生新区块时进行更新:
func (cli *CLI) send(from, to string, amount int) {
...
newBlock := bc.MineBlock(txs)
UTXOSet.Update(newBlock)
}
3 Merkle树
可以使用Merle树来优化交易查询。比特币使用Merkle树来获取交易哈希,然后保存在区块头中。到现在为止我们都是将每笔交易的哈希拼接起来,然后对拼接结果再计算最后的哈希。这是一种获取区块交易唯一证明的一种好方法,但是它没有利用到Merkle树的优点。
让我们看一下Merkle树的样子:
每个区块有一棵Merkle树。Merkle树的最底层是叶子节点,区块中每笔交易的哈希对应一个叶子节点。从最底层往上,最底层的每俩个相邻节点的哈希值拼接起来再计算哈希,新哈希成为上一层节点的哈希值。如果一层节点数为奇数2N+1,则单出来的那一个节点不需要拼接就计算哈希。最上层只有一个节点叫做根哈希。跟哈希作为区块所有交易的有效证明保存在区块头中。
Merkle树的好处是一个节点为了做交易验证,它可以不用下载整个区块链数据,而只需要下载交易哈希,一个Merkle根哈希,以及对应的Merkle树路径。
开始写代码。
type MerkleTree struct {
RootNode *MerkleNode
}
type MerkleNode struct {
Left *MerkleNode
Right *MerkleNode
Data []byte
}
MerleNode是Merkle树种的节点结构,它包括做节点、右节点和携带的数据。MerleTree就是Merkle树根。
创建新节点的方法:
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 transactions [][]byte
for _, tx := range b.Transactions {
transactions = append(transactions, tx.Serialize())
}
mTree := NewMerkleTree(transactions)
return mTree.RootNode.Data
}
4 实验及验证
源码见https://jeiwan.cc/posts/building-blockchain-in-go-part-6/
运行结果:
F:\GoPro\src\TBCPro\Transaction2>go_build_TBCPro_Transaction2.exe createblockchain -address 1HyAvXuteMM9nKrrfiMpFuz1q6zdSUEbbe
000049206a10f13db2ed689a096efc8f9b12035be1289fd15ac76125174b70fe
114204
Done!
F:\GoPro\src\TBCPro\Transaction2>go_build_TBCPro_Transaction2.exe getbalance -address 1HyAvXuteMM9nKrrfiMpFuz1q6zdSUEbbe
Balance of '1HyAvXuteMM9nKrrfiMpFuz1q6zdSUEbbe': 10
F:\GoPro\src\TBCPro\Transaction2>go_build_TBCPro_Transaction2.exe send -from 1HyAvXuteMM9nKrrfiMpFuz1q6zdSUEbbe -to 15pVcxBFoTQES6Qnya1TWR81m2kVKuaBM3 -amount 4
00003b24c9b405cd37edfd025a4dd3272bd9637747e4fb6694c3af901b95bf6f
73217
Success!
F:\GoPro\src\TBCPro\Transaction2>go_build_TBCPro_Transaction2.exe getbalance -address 1HyAvXuteMM9nKrrfiMpFuz1q6zdSUEbbe
Balance of '1HyAvXuteMM9nKrrfiMpFuz1q6zdSUEbbe': 16
F:\GoPro\src\TBCPro\Transaction2>go_build_TBCPro_Transaction2.exe getbalance -address 15pVcxBFoTQES6Qnya1TWR81m2kVKuaBM3
Balance of '15pVcxBFoTQES6Qnya1TWR81m2kVKuaBM3': 4
这里创建创世区块后,账号 1HyAvXuteMM9nKrrfiMpFuz1q6zdSUEbbe 分配了10金币。然后它发送了4个金币给第二个账号,由于有区块奖励,最终它的金额时16。
5 结论
我们实现了基于区块链的加密数字货币的几乎所有特性。我们有区块链、地址、挖矿以及交易。下一节我们将实现分布式这个很重要的特性,不要走开。