1.前言
前面我们已经实现了区块链数据结构、存储、共识、交易,我们接下来奖实现奖励机制。前面的代码只有创世块可以得到10的奖励,后面的并没有交易。
2.知识点
知识点 | 学习网页 | 特性 |
---|---|---|
UTXO | UTXO设置 | 未使用的事务处理 |
Merkle Tree | 梅克尔树 | Merkle树的好处是节点可以在不下载整个块的情况下验证某个事务的成员资格 |
交易 | 目的 | 输入 | 输出 | 签名 | 差额 |
---|---|---|---|---|---|
T0 | A转给B | 他人向A交易的输出 | B账号可以使用该交易 | A签确认 | 输入减去输出,为交易服务费 |
T1 | B转给C | T0的输出 | C账户可以使用该交易 | B签名确认 | 输入减去输出,为交易服务费 |
··· | X转给Y | 他人向X交易的输出 | Y账户可以使用该交易 | X签名确认 | 输入减去输出,为交易服务费 |
3.代码实现
奖励
在之前的文章中我们跳过的一件小事就是采矿奖励。而且我们已经拥有了实现它的一切。
奖励只是一个coinbase交易。当挖掘节点开始挖掘新块时,它会从队列中获取事务,并向它们添加coinbase事务。coinbase事务的唯一输出包含矿工的公钥哈希。
实施奖励与更新send命令一样简单:
//交易
func (cli *CLI) send(from, to string, amount int) {
//检验发送钱包地址
if !wallet.ValidateAddress(from) {
log.Panic("ERROR: Sender address is not valid")
}
//校验接收钱包地址
if !wallet.ValidateAddress(to) {
log.Panic("ERROR: Recipient address is not valid")
}
bc :=block.NewBlockchain()
defer block.Close(bc)
//bc := block.NewBlockchain()
UTXOSet := block.UTXOSet{bc}
tx := block.NewUTXOTransaction(from, to, amount, &UTXOSet)
cbTx := block.NewCoinbaseTX(from, "")
txs := []*block.Transaction{cbTx, tx}
newBlock := bc.MineBlock(txs)
UTXOSet.Update(newBlock)
fmt.Println("Success!")
}
UTXO设置
在第3部分:持久性和CLI中,我们研究了比特币核心在数据库中存储块的方式。据说块存储在blocks数据库中,事务输出存储在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?
考虑Blockchain.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
}
}
...
}
该函数查找未使用输出的事务。由于交易以块形式存储,因此它会遍历区块链中的每个块并检查其中的每个交易。截至2017年9月18日,比特币中有485,860个块,整个数据库需要140+ Gb的磁盘空间。这意味着必须运行一个完整节点来验证事务。而且,验证事务将需要迭代许多块。
这个问题的解决方案是有一个只存储未使用输出的索引,这就是UTXO集合所做的事情:这是一个从所有区块链事务构建而成的缓存(通过迭代块,是的,但是这只能完成一次),并且稍后用于计算余额并验证新的交易。截至2017年9月,UTXO约为2.7 Gb。
好吧,让我们想想我们需要改变以实现UTXO集。目前,以下方法用于查找交易:
- Blockchain.FindUnspentTransactions – 查找未使用输出的交易的主要功能。这是所有块迭代发生的这个函数。
- Blockchain.FindSpendableOutputs – 创建新事务时使用此功能。如果找到足够数量的输出持有所需的数量。用途Blockchain.FindUnspentTransactions。
- Blockchain.FindUTXO – 发现公用密钥哈希的未使用输出,用于实现平衡。用途Blockchain.FindUnspentTransactions。
- Blockchain.FindTransaction – 通过ID在区块链中查找交易。它遍历所有块直到找到它。
正如你所看到的,所有的方法遍历数据库中的块。但是现在我们无法改进它们,因为UTXO集不存储所有事务,但只存储那些没有使用输出的事务。因此,它不能用于Blockchain.FindTransaction。
所以,我们需要以下方法:
- Blockchain.FindUTXO – 通过迭代块来查找所有未使用的输出。
- UTXOSet.Reindex- 用于FindUTXO查找未使用的输出,并将它们存储在数据库中。这是缓存发生的地方。
- UTXOSet.FindSpendableOutputs- 模拟Blockchain.FindSpendableOutputs,但使用UTXO设置。
- UTXOSet.FindUTXO- 模拟Blockchain.FindUTXO,但使用UTXO设置。
- Blockchain.FindTransaction 保持不变。
因此,两个最常用的函数将从现在起使用缓存!我们开始编码。
type UTXOSet struct {
Blockchain *Blockchain
}
我们将使用单个数据库,但我们会将UTXO集存储在不同的存储桶中。因此,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集。首先,它会删除存在的桶,然后从块链中获取所有未使用的输出,最后将输出保存到存储桶中。
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
}
或检查余额:
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方法的稍微修改版本。这些Blockchain方法不再需要。
拥有UTXO集意味着我们的数据(交易)现在被分割到存储区中:实际交易存储在区块链中,未消耗的输出存储在UTXO集中。这种分离需要坚实的同步机制,因为我们希望UTXO集始终被更新并存储最近事务的输出。但我们不希望每次开挖新块时都要重新索引,因为这是我们想要避免的这些频繁的区块链扫描。因此,我们需要一个更新UTXO集合的机制:
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())
}
})
}
该方法看起来很大,但它做的很简单。当一个新块被挖掘时,UTXO集应该被更新。更新意味着消除用尽的产出并增加新开采交易的未用输出。如果一个事务的输出被删除,不包含更多的输出,它也会被删除。非常简单!
现在让我们在需要的地方使用UTXO集合:
func (cli *CLI) createBlockchain(address string) {
...
bc := CreateBlockchain(address)
defer bc.db.Close()
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()
...
}
在新的区块链被创建之后,重新编排就会发生。现在,这是唯一使用的地方Reindex,尽管在这里看起来过多,因为在区块链开始时,只有一个区块有一个事务,并且Update可以用来代替。但是我们可能需要未来的驯化机制。
func (cli *CLI) send(from, to string, amount int) {
...
newBlock := bc.MineBlock(txs)
UTXOSet.Update(newBlock)
}
UTXO集在开采新块之后进行更新。
用命令生成3个钱包地址:
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createwallet
Your new address: 1435pGo4kqRUjjPVogbKRdpqE62edRxCvz
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createwallet
Your new address: 14dFY3kYuS3Y8mU5PsiLio2LHng4MshJFa
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createwallet
Your new address: 1bfUDETpPzorw79VqzVfcsu4exqGc32e2
第一个地址创造一个创世块:
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createblockchain -address 1435pGo4kqRUjjPVogbKRdpqE62edRxCvz
Dig into mine 003f6d91231548bd0bf4d2ad64b6fd6ffcefa531d328ba0d7be6d5a729b1a2e4
Done!
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address 1435pGo4kqRUjjPVogbKRdpqE62edRxCvz
Balance of '1435pGo4kqRUjjPVogbKRdpqE62edRxCvz': 10
交易1:
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go send -from 1435pGo4kqRUjjPVogbKRdpqE62edRxCvz -to 14dFY3kYuS3Y8mU5PsiLio2LHng4MshJFa -amount 2
Dig into mine 000212e9c719e91712e9e8e0a4cb421ec213871d52fc0de9f038720c4e22f124
Success!
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address 1435pGo4kqRUjjPVogbKRdpqE62edRxCvz
Balance of '1435pGo4kqRUjjPVogbKRdpqE62edRxCvz': 18
交易2:
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go send -from 1435pGo4kqRUjjPVogbKRdpqE62edRxCvz -to 14dFY3kYuS3Y8mU5PsiLio2LHng4MshJFa -amount 8
Dig into mine 0014e26c60dc44aebf8dab512d80f1b90f8f4c7bcfc473fdd9e11ebd792ff4bc
Success!
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address 1435pGo4kqRUjjPVogbKRdpqE62edRxCvz
Balance of '1435pGo4kqRUjjPVogbKRdpqE62edRxCvz': 20
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address 14dFY3kYuS3Y8mU5PsiLio2LHng4MshJFa
Balance of '14dFY3kYuS3Y8mU5PsiLio2LHng4MshJFa': 10
太好了!该1435pGo4kqRUjjPVogbKRdpqE62edRxCvz地址收到奖励3次:
- 第一次奖励记录创世块。
- 第二次奖励记录交易一。
- 第三次奖励记录交易二。
目前为止我们把区块链除分布式网络部分,简单基本功能都完成了。后续降讲解网络部分。