源碼參考
https://github.com/huangzhenshi/BuildBitCoinSystemGo/tree/master/blockchain_learn/chapter6
參考博客
http://m.itdecent.cn/p/bfda51cd340d
發(fā)起一筆正常交易的構(gòu)建過(guò)程:
- 校驗(yàn)發(fā)起人、收款人的Address是否有效
- 檢查發(fā)起人是否有足夠的支付余額,并且返回相應(yīng)的用來(lái)消費(fèi)的outputs(map結(jié)構(gòu))
- 余額充足的話交易成立,會(huì)以outputs來(lái)構(gòu)建這筆交易的inputs,以及構(gòu)建該筆交易的outputs
- 把inputs和outputs構(gòu)建成一筆交易,再求Hash值,作為該筆交易的Txid
- 再對(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ū)別在于
- Blockchain里面的key存儲(chǔ)的是block的hash,value對(duì)應(yīng)的是整個(gè)Block,而UTXOSet的,key為transactionID,value是這個(gè)transaction里面的所有的未被消費(fèi)掉的TXOutputs的集合。
- UTXOSet不需要 tip這個(gè)字段,因?yàn)閁TXO的定位就是未消費(fèi)的output的索引,而且這個(gè)字段可以從原生的Blockchain里面很快獲取到,沒(méi)有必要重復(fù)存儲(chǔ)
- UTXOSet的map里面存儲(chǔ)的不是所有的交易,只存儲(chǔ)包含未花費(fèi)bitcoin的交易
- 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):
- 過(guò)濾:每次交易的時(shí)候只需要遍歷有余額的比特幣交易,原生的區(qū)塊鏈被分為兩部分:沒(méi)有任何余額的比特幣交易、有余額的比特幣交易(UTXOSet)。而且每次遍歷UTXOSet的時(shí)候,只要剩余金額大于交易金額,遍歷就可以break了,找到了足夠支付當(dāng)前交易的余額。也就是說(shuō),轉(zhuǎn)賬的金額越小,或者轉(zhuǎn)賬發(fā)起人擁有的比特幣越多,遍歷的時(shí)間就越短。
- UTXOSet中的交易不包含該筆交易的輸入源,所以計(jì)算交易的時(shí)候占用內(nèi)存更小
缺點(diǎn):
- 每次發(fā)起一次比特幣的交易的時(shí)候(包括轉(zhuǎn)賬和挖礦獎(jiǎng)勵(lì)),都需要更新這個(gè)UTXOSet,不過(guò)更新的成本很低,因?yàn)楦碌臅r(shí)候是知道所有input的Txid,可以根據(jù)鍵值對(duì)快速查找,新增也很方便,把所有的output丟進(jìn)去就可以了
- 需要額外的存儲(chǔ)空間來(lái)存儲(chǔ)這些未消費(fèi)的UTXOSet
UTXO的初始化和交易更新
- 初始化
根據(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 {}
- 更新:每次發(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):
- 每次發(fā)起一次比特幣的交易的時(shí)候(相互轉(zhuǎn)賬或者挖礦獎(jiǎng)勵(lì)),都需要更新這個(gè)UTXOSet,不過(guò)更新的成本很低,因?yàn)楦碌臅r(shí)候是知道所有input的Txid,可以根據(jù)鍵值對(duì)快速查找,新增也很方便,把所有的output丟進(jìn)去就可以了
- 需要額外的存儲(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)
}