# 前言
前面為了轉賬,需要迭代整個區塊鏈,查找未花費的輸出,這要求發起交易者必須運行一個全節點,截止 2017 年 9 月 18 日,在比特幣中已經有 485,860 個塊,整個數據庫所需磁盤空間超過 140 Gb。這是非常困難的。
整個問題的解決方案是有一個僅有未花費輸出的索引,這就是 UTXO 集要做的事情:這是一個從所有區塊鏈交易中構建(對區塊進行迭代,但是只須在創建區塊鏈時候做一次)而來的緩存,然后用它來計算余額和驗證新的交易。截止 2017 年 9 月,UTXO 集大概有 2.7 Gb,遠小于整個節點。
# 順便實現下挖礦獎勵
我們的區塊鏈由交易發起者自行完成挖礦,所以獎勵給到交易發起者即可。
挖礦獎勵,實際上就是一筆 coinbase 交易。
~~~
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, "")//創建一個coninbase交易,給from發送一個固定數量的獎勵。
txs := []*Transaction{cbTx, tx}//將coninbase交易和實際交易打包到一起
newBlock := bc.MineBlock(txs)//挖礦
fmt.Println("交易成功!")
}
~~~
>bitcoin的挖礦由專職礦工完成。交易發起者將交易丟進交易池中,礦工從交易池中取出一部分交易,打包挖礦,獲得獎勵。獎勵幣數量是不固定的。
# 實現UTXO
UTXO集定義
~~~
type UTXOSet struct {
Blockchain *Blockchain
}
~~~
UTXO元素是一個區塊鏈Blockchain的引用
## 取得UTXO并存儲到新的bucket中
~~~
func (u UTXOSet) Reindex() {
db := u.Blockchain.Db//與區塊鏈使用相同的數據庫
bucketName := []byte(utxoBucket)//新的bucket
err := db.Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket(bucketName)//如果存在,則刪除
_, err = tx.CreateBucket(bucketName)//創建新的bucket
})
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)
err = b.Put(key, outs.Serialize())//將交易ID-輸出存儲到bucket
}
})
}
~~~
這個方法僅僅在區塊鏈新建完成后,唯一一次被調用:當一個新的區塊鏈被創建以后,就會立刻進行重建索引。
~~~
func (cli *CLI) createBlockchain(address string) {
...
bc := CreateBlockchain(address)
defer bc.db.Close()
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()
...
}
~~~
## 轉賬的新方式~~~
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))//從bucket中讀取UTXO集
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {//循環UTXO集
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 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))//從bucket中讀取UTXO集
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {//循環UTXO集
outs := DeserializeOutputs(v)
for _, out := range outs.Outputs {
if out.IsLockedWithKey(pubKeyHash) {
UTXOs = append(UTXOs, out)
}
}
}
return nil
})
return UTXOs
}
~~~
## 同步機制
當挖出一個新塊時,應該更新 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 {//coinbase交易不更新到UTXO集中?那么挖礦獎勵金怎么辦?
for _, vin := range tx.Vin {
updatedOuts := TXOutputs{}//交易ID為vin.Txid的新的UTXO,將替換原來數據庫中交易ID為vin.Txid的UTXO
outsBytes := b.Get(vin.Txid)//從數據庫讀取的UTXO集中獲得被包含到輸入的輸出交易ID,一個vin中只有一個輸出索引
outs := DeserializeOutputs(outsBytes)
for outIdx, out := range outs.Outputs {//迭代數據庫該交易的所有索引的輸出。輸出集合可能有多個索引對應的輸出
if outIdx != vin.Vout {
//如果某條索引對于的輸出沒有被包含在輸入中,加入到新的最終輸出集合中。
//注意,包含到輸入中輸出最小單位是輸出的某條索引對應的輸出(索引從0開始)
updatedOuts.Outputs = append(updatedOuts.Outputs, out)//加入到新的已經花費的輸出組中
}
}
if len(updatedOuts.Outputs) == 0 {
//一個交易ID為vin.Txid的交易完成后,交易的輸出索引對應的輸出將被移除,如果結果是,該交易不再包含任何輸出,
//那么這筆交易的輸出應該被直接從UTXO數據庫中移除(UTXO鏈沒有必要留著空的節點)。
err := b.Delete(vin.Txid)
} else {
//如果交易ID為vin.Txid的交易在完成后,仍然有輸出,則更新到數據庫,替換該交易原來的UTXO集合
err := b.Put(vin.Txid, updatedOuts.Serialize())
}
}
}
//當然,交易將產生新的輸出,直接加入到UTXO中即可
newOutputs := TXOutputs{}
for _, out := range tx.Vout {//迭代Vout,獲得輸出索引對應的輸出
newOutputs.Outputs = append(newOutputs.Outputs, out)
}
err := b.Put(tx.ID, newOutputs.Serialize())//tx.ID為當前交易的ID
}
})
}
~~~
# Merkle樹實現
上如上面所提到的,完整的比特幣數據庫(也就是區塊鏈)需要超過 140 Gb 的磁盤空間。因為比特幣的去中心化特性,網絡中的每個節點必須是獨立,自給自足的,也就是每個節點必須存儲一個區塊鏈的完整副本。隨著越來越多的人使用比特幣,這條規則變得越來越難以遵守:因為不太可能每個人都去運行一個全節點。并且,由于節點是網絡中的完全參與者,它們負有相關責任:節點必須驗證交易和區塊。另外,要想與其他節點交互和下載新塊,也有一定的網絡流量需求。
在中本聰的[比特幣原始論文](https://link.jianshu.com/?t=https://bitcoin.org/bitcoin.pdf)中,對這個問題也有一個解決方案:簡易支付驗證(Simplified Payment Verification, SPV)。SPV 是比特幣輕節點,它不需要下載整個區塊鏈,也**不需要驗證區塊和交易**。相反,它會在區塊鏈查找交易(為了驗證支付),并且需要連接到一個全節點來檢索必要的數據。這個機制允許在全網只運行一個全節點的情況下有多個輕節點(輕錢包)。
為了實現 SPV,**需要有一個方式來檢查是否一個區塊包含了某筆交易,而無須下載整個區塊。這就是 Merkle 樹所要完成的事情。**
比特幣用 Merkle 樹來獲取交易哈希,哈希被保存在區塊頭中,并會用于工作量證明系統。
Merkle 樹的好處就是一個節點可以在不下載整個塊的情況下,驗證是否包含某筆交易。并且這些只需要一個當前交易哈希,一個 Merkle 樹根哈希和一個 Merkle 路徑(向全網請求獲得路徑),然后本地計算,即可檢查是否包含了某筆交易。
# 運行檢驗
## 轉賬

轉賬結果:

之所以轉賬后from的幣數量是17,是因為轉賬挖礦獎勵10幣,加上轉賬余額7幣,一共17個幣。
## 打印區塊鏈

# 優化Findtranaction
在很多場合需要根據交易ID,查找交易,函數Findtrasaction功能即如此。
為此,我們將<tx.ID,block.Hash>保存到數據庫,這樣,我們通過數據庫表utxoBlockBucket的key(tx.ID)可以查詢到block的ID,然后通過blocksBucket的Get,可以快速獲得交易。(原來是通過迭代blockchain來完成,效率很低)。這樣,也為后面輕節點通過網絡請求得到交易留下優化空間。
優化之后,節點仍然需要下載整個區塊鏈,才能查詢到交易。
## 表
const utxoBlockBucket?=?"chainstate\_blockid2tx"
## 修改Reindex

將UTXO和UTXOBlock都寫入數據庫
## 修改同步函數

更新UTXO的同時,同步UTXOBlock
## 修改FindtransactionForUTXO

## 修改其它代碼
- 重要更新說明
- linechain發布
- linechain新版設計
- 引言一
- 引言二
- 引言三
- vs-code設置及開發環境設置
- BoltDB數據庫應用
- 關于Go語言、VS-code的一些Tips
- 區塊鏈的架構
- 網絡通信與區塊鏈
- 單元測試
- 比特幣腳本語言
- 關于區塊鏈的一些概念
- 區塊鏈組件
- 區塊鏈第一版:基本原型
- 區塊鏈第二版:增加工作量證明
- 區塊鏈第三版:持久化
- 區塊鏈第四版:交易
- 區塊鏈第五版:實現錢包
- 區塊鏈第六版:實現UTXO集
- 區塊鏈第七版:網絡
- 階段小結
- 區塊鏈第八版:P2P
- P2P網絡架構
- 區塊鏈網絡層
- P2P區塊鏈最簡體驗
- libp2p建立P2P網絡的關鍵概念
- 區塊鏈結構層設計與實現
- 用戶交互層設計與實現
- 網絡層設計與實現
- 建立節點發現機制
- 向區塊鏈網絡請求區塊信息
- 向區塊鏈網絡發布消息
- 運行區塊鏈
- LineChain
- 系統運行流程
- Multihash
- 區塊鏈網絡的節點發現機制深入探討
- DHT
- Bootstrap
- 連接到所有引導節點
- Advertise
- 搜索其它peers
- 連接到搜到的其它peers
- 區塊鏈網絡的消息訂發布-訂閱機制深入探討
- LineChain:適用于智能合約編程的腳本語言支持
- LineChain:解決分叉問題
- LineChain:多重簽名
- libp2p升級到v0.22版本
- 以太坊基礎
- 重溫以太坊的樹結構
- 世界狀態樹
- (智能合約)賬戶存儲樹
- 交易樹
- 交易收據樹
- 小結
- 以太坊的存儲結構
- 以太坊狀態數據庫
- MPT
- 以太坊POW共識算法
- 智能合約存儲
- Polygon Edge
- block結構
- transaction數據結構
- 數據結構小結
- 關于本區塊鏈的一些說明
- UML工具-PlantUML
- libp2p介紹
- JSON-RPC
- docker制作:啟動多個應用系統
- Dockerfile
- docker-entrypoint.sh
- supervisord.conf
- docker run
- nginx.conf
- docker基礎操作整理
- jupyter計算交互環境
- git技巧一
- git技巧二
- 使用github項目的最佳實踐
- windows下package管理工具