第六章:挖矿和UTXOSet

源码参考

https://github.com/huangzhenshi/BuildBitCoinSystemGo/tree/master/blockchain_learn/chapter6

参考博客

https://www.jianshu.com/p/bfda51cd340d

发起一笔正常交易的构建过程:

  1. 校验发起人、收款人的Address是否有效
  2. 检查发起人是否有足够的支付余额,并且返回相应的用来消费的outputs(map结构)
  3. 余额充足的话交易成立,会以outputs来构建这笔交易的inputs,以及构建该笔交易的outputs
  4. 把inputs和outputs构建成一笔交易,再求Hash值,作为该笔交易的Txid
  5. 再对这笔交易的每个input逐个做签名,最终生成一个完整和有效的Transaction

主要耗费时间的是第二步,需要遍历所有的交易,直到找到足够的用来支付当前交易的 output余额,随着比特币交易的增多,需要遍历的工作量越来越大,而且有很多是无意义的遍历,因为很多被完全消费掉的交易(没有余额),每一次交易仍然会被遍历一次。所以引入UTXOSet,过滤掉没有余额的交易,从而提升交易的速度。

UTXOSet

UTXOSet的数据结构和Blockchain是一样的,都是一个map的结构

UTXOSet和Blockchain的区别在于

  1. Blockchain里面的key存储的是block的hash,value对应的是整个Block,而UTXOSet的,key为transactionID,value是这个transaction里面的所有的未被消费掉的TXOutputs的集合。
  2. UTXOSet不需要 tip这个字段,因为UTXO的定位就是未消费的output的索引,而且这个字段可以从原生的Blockchain里面很快获取到,没有必要重复存储
  3. UTXOSet的map里面存储的不是所有的交易,只存储包含未花费bitcoin的交易
  4. UTXOSet的map里面存储的不是完整的交易,完整的一个交易由:输入、输出、Txid组成,而UTXOSet里面存储的交易只有Txid(key)和输出(value),没有输入
type UTXOSet struct {
    Blockchain *Blockchain
}
type Blockchain struct {
    tip []byte
    db  *bolt.DB
}

UTXOSet的作用:过滤和缩小查询时候的内存空间。用额外的存储空间换查询速度,从而提升比特币交易的速度。
优点:

  1. 过滤:每次交易的时候只需要遍历有余额的比特币交易,原生的区块链被分为两部分:没有任何余额的比特币交易、有余额的比特币交易(UTXOSet)。而且每次遍历UTXOSet的时候,只要剩余金额大于交易金额,遍历就可以break了,找到了足够支付当前交易的余额。也就是说,转账的金额越小,或者转账发起人拥有的比特币越多,遍历的时间就越短。
  2. UTXOSet中的交易不包含该笔交易的输入源,所以计算交易的时候占用内存更小

缺点:

  1. 每次发起一次比特币的交易的时候(包括转账和挖矿奖励),都需要更新这个UTXOSet,不过更新的成本很低,因为更新的时候是知道所有input的Txid,可以根据键值对快速查找,新增也很方便,把所有的output丢进去就可以了
  2. 需要额外的存储空间来存储这些未消费的UTXOSet

UTXO的初始化和交易更新

  1. 初始化
    根据一个完整的区块链,来初始化构建一个UTXO,调用 Reindex()方法
func (u UTXOSet) Reindex() {
    UTXO := u.Blockchain.FindUTXO()
    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())
        }
    })
}

func (bc *Blockchain) FindUTXO() map[string]TXOutputs {}
  1. 更新:每次发生交易的时候都需要更新这个UTXOSET,不过计算的开销并不大,都是通过键值对查找和替换
        //遍历所有的交易
        for _, tx := range block.Transactions {
            //如果是挖矿交易的话,只需要把output加进去就可以了
            //如果是普通交易的话,需要先把所有的input都逆推到Txid,再一个个delete掉,最后再把所有的output加进去
            //这里的代码就是删除对应的本次交易消费掉的output
            if tx.IsCoinbase() == false {
                //遍历所有的Vin,通过Vin.Txid找到对应的UTXO中的outs
                //建立一个新的updatedOuts,通过过滤掉Vin消费的out,剩下的丢到updatedOuts里面,最后替换掉就ok
                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)
                    } else {
                        err := b.Put(vin.Txid, updatedOuts.Serialize())
                    }

                }
            }
            //不管是挖矿交易还是正常交易,都需要把新产生的output封装在当前Txid里面,然后put到UTXOSet中
            newOutputs := TXOutputs{}
            for _, out := range tx.Vout {
                newOutputs.Outputs = append(newOutputs.Outputs, out)
            }
            err := b.Put(tx.ID, newOutputs.Serialize())
        }
    })

UTXO提供的方法 FindSpendableOutputs

优点:如果A用户比特币充裕,足够支持当前amount金额的交易,则不需要遍历所有的output,只要遍历到够花就break了
适用业务场景: 该方法适用于交易和查询余额的时候,查询账户余额的时候需要遍历整个UTXOSet
缺点:

  1. 每次发起一次比特币的交易的时候(相互转账或者挖矿奖励),都需要更新这个UTXOSet,不过更新的成本很低,因为更新的时候是知道所有input的Txid,可以根据键值对快速查找,新增也很方便,把所有的output丢进去就可以了
  2. 需要额外的存储空间来存储这些未消费的outputs
// 注意这里读取的是数据库
// 返回值:第一个是该用户的全部余额或者可供消费的output集合的总金额,第二个是用来即将被消费的属于该用户的output
// 可能是(比如要转账10比特币,该用户正好拥有10个比特币),也可能不是该用户的所有output(比如A有100个比特币,只需要转出10个)
// 该方法有一个特别好的地方,如果A用户比特币充裕,足够支持当前amount金额的交易,则不需要遍历所有的output,只要遍历到够花就break了
func (u UTXOSet) FindSpendableOutputs(pubkeyHash []byte, amount int) (int, map[string][]int) {
    unspentOutputs := make(map[string][]int)
    accumulated := 0
    db := u.Blockchain.db
    //数据库中读取chainstate bucket,然后遍历
    err := db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(utxoBucket))
        c := b.Cursor()
        //遍历所有的包含有未花费output的交易
        for k, v := c.First(); k != nil; k, v = c.Next() {
            txID := hex.EncodeToString(k)
            outs := DeserializeOutputs(v)
            //遍历该交易中所有的未花费的output,按照pubkeyHash进行匹配,并且追加到unspentOutputs当中
            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
}

挖矿奖励

当一个区块链节点收到一笔记账消息的时候,会在这个记账信息的后面添加一笔交易,也就是给自己的Address添加挖矿奖励的交易

func (cli *CLI) checkAndRecord(from, to string, amount int) {
    bc := NewBlockchain()
    UTXOSet := UTXOSet{bc}
    tx := NewUTXOTransaction(from, to, amount, &UTXOSet)
    
    //添加一笔给自己奖励的交易,并且加入到[]Transaction中
    cbTx := NewCoinbaseTX(myaccount, "")
    txs := []*Transaction{cbTx, tx}
    newBlock := bc.MineBlock(txs)
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容