1. 程式人生 > >用go編寫區塊鏈系列之6--交易2

用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://github.com/Jeiwan/blockchain_go/tree/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 結論

我們實現了基於區塊鏈的加密數字貨幣的幾乎所有特性。我們有區塊鏈、地址、挖礦以及交易。下一節我們將實現分散式這個很重要的特性,不要走開。