区块链第六版:实现UTXO集

undefined前言

前面为了转账,需要迭代整个区块链,查找未花费的输出,这要求发起交易者必须运行一个全节点,截止 2017 年 9 月 18 日,在比特币中已经有 485,860 个块,整个数据库所需磁盘空间超过 140 Gb。这是非常困难的。

整个问题的解决方案是有一个仅有未花费输出的索引,这就是 UTXO 集要做的事情:这是一个从所有区块链交易中构建(对区块进行迭代,但是只须在创建区块链时候做一次)而来的缓存,然后用它来计算余额和验证新的交易。截止 2017 年 9 月,UTXO 集大概有 2.7 Gb,远小于整个节点。

undefined顺便实现下挖矿奖励

我们的区块链由交易发起者自行完成挖矿,所以奖励给到交易发起者即可。

挖矿奖励,实际上就是一笔 coinbase 交易。

  1. func (cli *CLI) send(from, to string, amount int) {
  2. ...
  3. bc := NewBlockchain()
  4. UTXOSet := UTXOSet{bc}
  5. defer bc.db.Close()
  6. tx := NewUTXOTransaction(from, to, amount, &UTXOSet)
  7. cbTx := NewCoinbaseTX(from, "")//创建一个coninbase交易,给from发送一个固定数量的奖励。
  8. txs := []*Transaction{cbTx, tx}//将coninbase交易和实际交易打包到一起
  9. newBlock := bc.MineBlock(txs)//挖矿
  10. fmt.Println("交易成功!")
  11. }

bitcoin的挖矿由专职矿工完成。交易发起者将交易丢进交易池中,矿工从交易池中取出一部分交易,打包挖矿,获得奖励。奖励币数量是不固定的。

undefined实现UTXO

UTXO集定义

  1. type UTXOSet struct {
  2. Blockchain *Blockchain
  3. }

UTXO元素是一个区块链Blockchain的引用

undefined取得UTXO并存储到新的bucket中

  1. func (u UTXOSet) Reindex() {
  2. db := u.Blockchain.Db//与区块链使用相同的数据库
  3. bucketName := []byte(utxoBucket)//新的bucket
  4. err := db.Update(func(tx *bolt.Tx) error {
  5. err := tx.DeleteBucket(bucketName)//如果存在,则删除
  6. _, err = tx.CreateBucket(bucketName)//创建新的bucket
  7. })
  8. UTXO := u.Blockchain.FindUTXO()//获得所有未花费输出
  9. err = db.Update(func(tx *bolt.Tx) error {
  10. b := tx.Bucket(bucketName)
  11. for txID, outs := range UTXO {
  12. key, err := hex.DecodeString(txID)
  13. err = b.Put(key, outs.Serialize())//将交易ID-输出存储到bucket
  14. }
  15. })
  16. }

这个方法仅仅在区块链新建完成后,唯一一次被调用:当一个新的区块链被创建以后,就会立刻进行重建索引。

  1. func (cli *CLI) createBlockchain(address string) {
  2. ...
  3. bc := CreateBlockchain(address)
  4. defer bc.db.Close()
  5. UTXOSet := UTXOSet{bc}
  6. UTXOSet.Reindex()
  7. ...
  8. }

undefined转账的新方式~~~

func (u UTXOSet) FindSpendableOutputs(pubkeyHash []byte, amount int) (int, map[string][]int) {

unspentOutputs := make(map[string][]int)

accumulated := 0

db := u.Blockchain.Db

  1. err := db.View(func(tx *bolt.Tx) error {
  2. b := tx.Bucket([]byte(utxoBucket))//从bucket中读取UTXO集
  3. c := b.Cursor()
  4. for k, v := c.First(); k != nil; k, v = c.Next() {//循环UTXO集
  5. txID := hex.EncodeToString(k)
  6. outs := DeserializeOutputs(v)
  7. for outIdx, out := range outs.Outputs {
  8. if out.IsLockedWithKey(pubkeyHash) && accumulated < amount {
  9. accumulated += out.Value
  10. unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)
  11. }
  12. }
  13. }
  14. })
  15. return accumulated, unspentOutputs

}

  1. ## 查询余额的新方式

func (u UTXOSet) FindUTXO(pubKeyHash []byte) []TXOutput {

var UTXOs []TXOutput

db := u.Blockchain.Db

  1. err := db.View(func(tx *bolt.Tx) error {
  2. b := tx.Bucket([]byte(utxoBucket))//从bucket中读取UTXO集
  3. c := b.Cursor()
  4. for k, v := c.First(); k != nil; k, v = c.Next() {//循环UTXO集
  5. outs := DeserializeOutputs(v)
  6. for _, out := range outs.Outputs {
  7. if out.IsLockedWithKey(pubKeyHash) {
  8. UTXOs = append(UTXOs, out)
  9. }
  10. }
  11. }
  12. return nil
  13. })
  14. return UTXOs

}

  1. ## 同步机制
  2. 当挖出一个新块时,应该更新 UTXO 集。
  3. 更新意味着移除已花费输出,并从新挖出来的交易中加入未花费输出。
  1. func (u UTXOSet) Update(block *Block) {//block为挖出的新区块
  2. db := u.Blockchain.db
  3. err := db.Update(func(tx *bolt.Tx) error {
  4. b := tx.Bucket([]byte(utxoBucket))
  5. for _, tx := range block.Transactions {//迭代新区块里面的所有交易
  6. if tx.IsCoinbase() == false {//coinbase交易不更新到UTXO集中?那么挖矿奖励金怎么办?
  7. for _, vin := range tx.Vin {
  8. updatedOuts := TXOutputs{}//交易ID为vin.Txid的新的UTXO,将替换原来数据库中交易ID为vin.Txid的UTXO
  9. outsBytes := b.Get(vin.Txid)//从数据库读取的UTXO集中获得被包含到输入的输出交易ID,一个vin中只有一个输出索引
  10. outs := DeserializeOutputs(outsBytes)
  11. for outIdx, out := range outs.Outputs {//迭代数据库该交易的所有索引的输出。输出集合可能有多个索引对应的输出
  12. if outIdx != vin.Vout {
  13. //如果某条索引对于的输出没有被包含在输入中,加入到新的最终输出集合中。
  14. //注意,包含到输入中输出最小单位是输出的某条索引对应的输出(索引从0开始)
  15. updatedOuts.Outputs = append(updatedOuts.Outputs, out)//加入到新的已经花费的输出组中
  16. }
  17. }
  18. if len(updatedOuts.Outputs) == 0 {
  19. //一个交易ID为vin.Txid的交易完成后,交易的输出索引对应的输出将被移除,如果结果是,该交易不再包含任何输出,
  20. //那么这笔交易的输出应该被直接从UTXO数据库中移除(UTXO链没有必要留着空的节点)。
  21. err := b.Delete(vin.Txid)
  22. } else {
  23. //如果交易ID为vin.Txid的交易在完成后,仍然有输出,则更新到数据库,替换该交易原来的UTXO集合
  24. err := b.Put(vin.Txid, updatedOuts.Serialize())
  25. }
  26. }
  27. }
  28. //当然,交易将产生新的输出,直接加入到UTXO中即可
  29. newOutputs := TXOutputs{}
  30. for _, out := range tx.Vout {//迭代Vout,获得输出索引对应的输出
  31. newOutputs.Outputs = append(newOutputs.Outputs, out)
  32. }
  33. err := b.Put(tx.ID, newOutputs.Serialize())//tx.ID为当前交易的ID
  34. }
  35. })
  36. }

undefinedMerkle树实现

上如上面所提到的,完整的比特币数据库(也就是区块链)需要超过 140 Gb 的磁盘空间。因为比特币的去中心化特性,网络中的每个节点必须是独立,自给自足的,也就是每个节点必须存储一个区块链的完整副本。随着越来越多的人使用比特币,这条规则变得越来越难以遵守:因为不太可能每个人都去运行一个全节点。并且,由于节点是网络中的完全参与者,它们负有相关责任:节点必须验证交易和区块。另外,要想与其他节点交互和下载新块,也有一定的网络流量需求。

在中本聪的比特币原始论文中,对这个问题也有一个解决方案:简易支付验证(Simplified Payment Verification, SPV)。SPV 是比特币轻节点,它不需要下载整个区块链,也不需要验证区块和交易。相反,它会在区块链查找交易(为了验证支付),并且需要连接到一个全节点来检索必要的数据。这个机制允许在全网只运行一个全节点的情况下有多个轻节点(轻钱包)。

为了实现 SPV,需要有一个方式来检查是否一个区块包含了某笔交易,而无须下载整个区块。这就是 Merkle 树所要完成的事情。

比特币用 Merkle 树来获取交易哈希,哈希被保存在区块头中,并会用于工作量证明系统。

Merkle 树的好处就是一个节点可以在不下载整个块的情况下,验证是否包含某笔交易。并且这些只需要一个当前交易哈希,一个 Merkle 树根哈希和一个 Merkle 路径(向全网请求获得路径),然后本地计算,即可检查是否包含了某笔交易。

undefined运行检验

undefined转账

区块链第六版:实现UTXO集 - 图1

转账结果:

区块链第六版:实现UTXO集 - 图2

之所以转账后from的币数量是17,是因为转账挖矿奖励10币,加上转账余额7币,一共17个币。

undefined打印区块链

区块链第六版:实现UTXO集 - 图3

undefined优化Findtranaction

在很多场合需要根据交易ID,查找交易,函数Findtrasaction功能即如此。

为此,我们将<tx.ID,block.Hash>保存到数据库,这样,我们通过数据库表utxoBlockBucket的key(tx.ID)可以查询到block的ID,然后通过blocksBucket的Get,可以快速获得交易。(原来是通过迭代blockchain来完成,效率很低)。这样,也为后面轻节点通过网络请求得到交易留下优化空间。

优化之后,节点仍然需要下载整个区块链,才能查询到交易。

undefined表

const utxoBlockBucket = "chainstate_blockid2tx"

undefined修改Reindex

区块链第六版:实现UTXO集 - 图4

将UTXO和UTXOBlock都写入数据库

undefined修改同步函数

区块链第六版:实现UTXO集 - 图5

更新UTXO的同时,同步UTXOBlock

undefined修改FindtransactionForUTXO

区块链第六版:实现UTXO集 - 图6

undefined修改其它代码