在持久化和CLI中,曾经提到比特币将block存储在数据库:block存储在blocks数据库;交易TXO存储在chainstate数据库。现在重新回顾下chainstate的结构:
- 'c' + 32-byte transaction hash -> unspent transaction output record for that transaction
- 'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs
在此之前,我们实现了交易,但是没有将交易信息存储到chainstate数据库。现在,我们来实现此机制。
chainstate数据库不存储交易本身,而是存储所有UTXO,称为UTXO集合。此外,chainstate数据库也会存储“the block hash up to which the database represents the unspent transaction outputs”,不过目前用不到block高度的的信息,故暂不实现。
那么,UTXO集合有什么用呢?
让我们回顾下FindUnspentTransactions方法:
func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []Transaction {
...
bci := bc.Iterator()
for {
block := bci.Next()
for _, tx := range block.Transactions {
...
}
if len(block.PrevBlockHash) == 0 {
break
}
}
...
}
该方法返回所有UTXO。由于交易存储在block中,因此需要遍历blockchain中的所有block,对block中的所有交易进行检查。截止2017/9/18,比特币有485860个block,整个数据库大小超过140GB。这意味着需要遍历整个数据库中的所有block对交易进行检查。可想而知效率会非常低!
为了解决此问题,我们需要对UTXO建立索引,这就是所谓的UTXO集合。UTXO集合其实是一个缓存,遍历blockchain中所有交易得到的,用于计算余额以及交易验证。截止2017/9,比特币的UTXO集合大小约为2.7GB。
目前,下述方法用于查找交易:
- Blockchain.FindUnspentTransactions – 遍历所有block返回UTXO
- Blockchain.FindSpendableOutputs – 新交易创建时,通过此函数找到满足要求的可以供消费的交易。此函数使用到了Blockchain.FindUnspentTransactions
- Blockchain.FindUTXO – 返回指定公钥hash值的所有UTXO,用于计算余额。此函数使用到了Blockchain.FindUnspentTransactions
- Blockchain.FindTransaction - 根据交易ID查找交易。该函数会遍历所有block进行查找。
上述方法均会遍历整个数据库中所有block,但由于UTXO集合仅仅存储UTXO不存储所有交易,因此UTXO集合无法为上述所有方法带来改善,如Blockchain.FindTransaction方法。
所以,我们考虑使用下面的新方法:
- Blockchain.FindUTXO – 遍历block返回所有UTXO
- UTXOSet.Reindex – 将Blockchain.FindUTXO结果存储到数据库
- UTXOSet.FindSpendableOutputs - 和Blockchain. FindSpendableOutputs类似,只不过基于UTXO集合进行计算
- UTXOSet.FindUTXO - 和Blockchain.FindUTXO 类似,只不过基于UTXO集合进行计算
- Blockchain.FindTransaction保持不变
现在,两个使用最频繁的方法均使用了UTXO集合做缓存:
type UTXOSet struct {
Blockchain *Blockchain
}
我们仍使用同一个数据库,将UTXO集合存储在不同的bucket中。因此,UTXOSet和Blockchain相伴而生。
func (u UTXOSet) Reindex() {
db := u.Blockchain.db
bucketName := []byte(utxoBucket)
err := db.Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket(bucketName)
_, err = tx.CreateBucket(bucketName)
})
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())
}
})
}
首先,若UTXO集合存在,将其删除;然后获取所有UTXO;最后将其保存到bucket中。
Blockchain.FindUTXO几乎与Blockchain.FindUnspentTransactions完全一致,只不过现在该方法返回TransactionID → TransactionOutputs对。
现在,发送货币过程可以用到UTXO集合:
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))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
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
}
获取余额过程也可以用到UTXO集合:
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))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
outs := DeserializeOutputs(v)
for _, out := range outs.Outputs {
if out.IsLockedWithKey(pubKeyHash) {
UTXOs = append(UTXOs, out)
}
}
}
return nil
})
return UTXOs
}
之前Blockchain的那些方法已经不需要了,现在开始使用这些新方法。
UTXO集合的出现意味着交易信息分为两部分存储:实际交易数据存储在blockchain中,UTXO数据存储在UTXO集合中。为了保持数据一致性的同时避免每次挖到一个新的block都重建UTXO集合(不希望频繁扫描blockchain),需要一个数据同步机制:
func (u UTXOSet) Update(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 {
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())
}
}
}
newOutputs := TXOutputs{}
for _, out := range tx.Vout {
newOutputs.Outputs = append(newOutputs.Outputs, out)
}
err := b.Put(tx.ID, newOutputs.Serialize())
}
})
}
该方法看上去很长,但实际很简单。当新的block生成时,需要更新UTXO集合。更新意味着删除已消费的TXO,同时将新生成的交易中的UTXO添加进来。若某个交易的UTXO全部被删除,该交易也会随之被移除。
func (cli *CLI) createBlockchain(address string) {
...
bc := CreateBlockchain(address)
defer bc.db.Close()
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()
...
}
当blockchain被创建时,需要创建UTXO集合。现在,这是唯一需要进行Reindex的地方。虽然blockchain一开始仅有一个block以及一个交易,这时仅需要执行Update没必要进行Reindex,但是将来可能需要这个重建机制。
func (cli *CLI) send(from, to string, amount int) {
...
newBlock := bc.MineBlock(txs)
UTXOSet.Update(newBlock)
}
新的block生成时,更新UTXO集合。
下面看看运行情况:
$ blockchain_go createblockchain -address 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1
00000086a725e18ed7e9e06f1051651a4fc46a315a9d298e59e57aeacbe0bf73
Done!
$ blockchain_go send -from 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1 -to 12DkLzLQ4B3gnQt62EPRJGZ38n3zF4Hzt5 -amount 6
0000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21b
Success!
$ blockchain_go send -from 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1 -to 12ncZhA5mFTTnTmHq1aTPYBri4jAK8TacL -amount 4
000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433
Success!
$ blockchain_go getbalance -address 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1
Balance of '1F4MbuqjcuJGymjcuYQMUVYB37AWKkSLif': 20
$ blockchain_go getbalance -address 12DkLzLQ4B3gnQt62EPRJGZ38n3zF4Hzt5
Balance of '1XWu6nitBWe6J6v6MXmd5rhdP7dZsExbx': 6
$ blockchain_go getbalance -address 12ncZhA5mFTTnTmHq1aTPYBri4jAK8TacL
Balance of '13UASQpCR8Nr41PojH8Bz4K6cmTCqweskL': 4
1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1收到3次奖励:
- 第1次奖励,挖到了genesis block
- 第2次奖励,挖到了block:0000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21b
- 第3次奖励,挖到了block:000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433