第六章:挖礦和UTXOSet

源碼參考

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

參考博客

http://m.itdecent.cn/p/bfda51cd340d

發(fā)起一筆正常交易的構(gòu)建過(guò)程:

  1. 校驗(yàn)發(fā)起人、收款人的Address是否有效
  2. 檢查發(fā)起人是否有足夠的支付余額,并且返回相應(yīng)的用來(lái)消費(fèi)的outputs(map結(jié)構(gòu))
  3. 余額充足的話交易成立,會(huì)以outputs來(lái)構(gòu)建這筆交易的inputs,以及構(gòu)建該筆交易的outputs
  4. 把inputs和outputs構(gòu)建成一筆交易,再求Hash值,作為該筆交易的Txid
  5. 再對(duì)這筆交易的每個(gè)input逐個(gè)做簽名,最終生成一個(gè)完整和有效的Transaction

主要耗費(fèi)時(shí)間的是第二步,需要遍歷所有的交易,直到找到足夠的用來(lái)支付當(dāng)前交易的 output余額,隨著比特幣交易的增多,需要遍歷的工作量越來(lái)越大,而且有很多是無(wú)意義的遍歷,因?yàn)楹芏啾煌耆M(fèi)掉的交易(沒(méi)有余額),每一次交易仍然會(huì)被遍歷一次。所以引入U(xiǎn)TXOSet,過(guò)濾掉沒(méi)有余額的交易,從而提升交易的速度。

UTXOSet

UTXOSet的數(shù)據(jù)結(jié)構(gòu)和Blockchain是一樣的,都是一個(gè)map的結(jié)構(gòu)

UTXOSet和Blockchain的區(qū)別在于

  1. Blockchain里面的key存儲(chǔ)的是block的hash,value對(duì)應(yīng)的是整個(gè)Block,而UTXOSet的,key為transactionID,value是這個(gè)transaction里面的所有的未被消費(fèi)掉的TXOutputs的集合。
  2. UTXOSet不需要 tip這個(gè)字段,因?yàn)閁TXO的定位就是未消費(fèi)的output的索引,而且這個(gè)字段可以從原生的Blockchain里面很快獲取到,沒(méi)有必要重復(fù)存儲(chǔ)
  3. UTXOSet的map里面存儲(chǔ)的不是所有的交易,只存儲(chǔ)包含未花費(fèi)bitcoin的交易
  4. UTXOSet的map里面存儲(chǔ)的不是完整的交易,完整的一個(gè)交易由:輸入、輸出、Txid組成,而UTXOSet里面存儲(chǔ)的交易只有Txid(key)和輸出(value),沒(méi)有輸入
type UTXOSet struct {
    Blockchain *Blockchain
}
type Blockchain struct {
    tip []byte
    db  *bolt.DB
}

UTXOSet的作用:過(guò)濾和縮小查詢時(shí)候的內(nèi)存空間。用額外的存儲(chǔ)空間換查詢速度,從而提升比特幣交易的速度。
優(yōu)點(diǎn):

  1. 過(guò)濾:每次交易的時(shí)候只需要遍歷有余額的比特幣交易,原生的區(qū)塊鏈被分為兩部分:沒(méi)有任何余額的比特幣交易、有余額的比特幣交易(UTXOSet)。而且每次遍歷UTXOSet的時(shí)候,只要剩余金額大于交易金額,遍歷就可以break了,找到了足夠支付當(dāng)前交易的余額。也就是說(shuō),轉(zhuǎn)賬的金額越小,或者轉(zhuǎn)賬發(fā)起人擁有的比特幣越多,遍歷的時(shí)間就越短。
  2. UTXOSet中的交易不包含該筆交易的輸入源,所以計(jì)算交易的時(shí)候占用內(nèi)存更小

缺點(diǎn):

  1. 每次發(fā)起一次比特幣的交易的時(shí)候(包括轉(zhuǎn)賬和挖礦獎(jiǎng)勵(lì)),都需要更新這個(gè)UTXOSet,不過(guò)更新的成本很低,因?yàn)楦碌臅r(shí)候是知道所有input的Txid,可以根據(jù)鍵值對(duì)快速查找,新增也很方便,把所有的output丟進(jìn)去就可以了
  2. 需要額外的存儲(chǔ)空間來(lái)存儲(chǔ)這些未消費(fèi)的UTXOSet

UTXO的初始化和交易更新

  1. 初始化
    根據(jù)一個(gè)完整的區(qū)塊鏈,來(lái)初始化構(gòu)建一個(gè)UTXO,調(diào)用 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. 更新:每次發(fā)生交易的時(shí)候都需要更新這個(gè)UTXOSET,不過(guò)計(jì)算的開(kāi)銷并不大,都是通過(guò)鍵值對(duì)查找和替換
        //遍歷所有的交易
        for _, tx := range block.Transactions {
            //如果是挖礦交易的話,只需要把output加進(jìn)去就可以了
            //如果是普通交易的話,需要先把所有的input都逆推到Txid,再一個(gè)個(gè)delete掉,最后再把所有的output加進(jìn)去
            //這里的代碼就是刪除對(duì)應(yīng)的本次交易消費(fèi)掉的output
            if tx.IsCoinbase() == false {
                //遍歷所有的Vin,通過(guò)Vin.Txid找到對(duì)應(yīng)的UTXO中的outs
                //建立一個(gè)新的updatedOuts,通過(guò)過(guò)濾掉Vin消費(fèi)的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())
                    }

                }
            }
            //不管是挖礦交易還是正常交易,都需要把新產(chǎn)生的output封裝在當(dāng)前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

優(yōu)點(diǎn):如果A用戶比特幣充裕,足夠支持當(dāng)前amount金額的交易,則不需要遍歷所有的output,只要遍歷到夠花就break了
適用業(yè)務(wù)場(chǎng)景: 該方法適用于交易和查詢余額的時(shí)候,查詢賬戶余額的時(shí)候需要遍歷整個(gè)UTXOSet
缺點(diǎn):

  1. 每次發(fā)起一次比特幣的交易的時(shí)候(相互轉(zhuǎn)賬或者挖礦獎(jiǎng)勵(lì)),都需要更新這個(gè)UTXOSet,不過(guò)更新的成本很低,因?yàn)楦碌臅r(shí)候是知道所有input的Txid,可以根據(jù)鍵值對(duì)快速查找,新增也很方便,把所有的output丟進(jìn)去就可以了
  2. 需要額外的存儲(chǔ)空間來(lái)存儲(chǔ)這些未消費(fèi)的outputs
// 注意這里讀取的是數(shù)據(jù)庫(kù)
// 返回值:第一個(gè)是該用戶的全部余額或者可供消費(fèi)的output集合的總金額,第二個(gè)是用來(lái)即將被消費(fèi)的屬于該用戶的output
// 可能是(比如要轉(zhuǎn)賬10比特幣,該用戶正好擁有10個(gè)比特幣),也可能不是該用戶的所有output(比如A有100個(gè)比特幣,只需要轉(zhuǎn)出10個(gè))
// 該方法有一個(gè)特別好的地方,如果A用戶比特幣充裕,足夠支持當(dāng)前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
    //數(shù)據(jù)庫(kù)中讀取chainstate bucket,然后遍歷
    err := db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(utxoBucket))
        c := b.Cursor()
        //遍歷所有的包含有未花費(fèi)output的交易
        for k, v := c.First(); k != nil; k, v = c.Next() {
            txID := hex.EncodeToString(k)
            outs := DeserializeOutputs(v)
            //遍歷該交易中所有的未花費(fèi)的output,按照pubkeyHash進(jìn)行匹配,并且追加到unspentOutputs當(dāng)中
            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
}

挖礦獎(jiǎng)勵(lì)

當(dāng)一個(gè)區(qū)塊鏈節(jié)點(diǎn)收到一筆記賬消息的時(shí)候,會(huì)在這個(gè)記賬信息的后面添加一筆交易,也就是給自己的Address添加挖礦獎(jiǎng)勵(lì)的交易

func (cli *CLI) checkAndRecord(from, to string, amount int) {
    bc := NewBlockchain()
    UTXOSet := UTXOSet{bc}
    tx := NewUTXOTransaction(from, to, amount, &UTXOSet)
    
    //添加一筆給自己獎(jiǎng)勵(lì)的交易,并且加入到[]Transaction中
    cbTx := NewCoinbaseTX(myaccount, "")
    txs := []*Transaction{cbTx, tx}
    newBlock := bc.MineBlock(txs)
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容