内容简介:有一件前面的章节中跳过了一个小细节,挖矿奖励。现在我们准备实现这个。这个奖励也就是coinbase交易。当一个节点开始挖新的区块时,它会把队列中的交易并准备好coinbase交易放到区块中。这笔coinbase交易也仅仅是一个包含了矿工的公钥hash的output。实现奖励很简单,只用更新一下send命令:
Reward(奖励)
有一件前面的章节中跳过了一个小细节,挖矿奖励。现在我们准备实现这个。
这个奖励也就是coinbase交易。当一个节点开始挖新的区块时,它会把队列中的交易并准备好coinbase交易放到区块中。这笔coinbase交易也仅仅是一个包含了矿工的公钥hash的output。
实现奖励很简单,只用更新一下send命令:
func (cli *CLI) send(from, to string, amount int) { ... bc := NewBlockchain() UTXOSet := UTXOSet{bc} defer bc.db.Close() tx := NewUTXOTransaction(from, to, amount, &UTXOSet) cbTx := NewCoinbaseTX(from, "") txs := []*Transaction{cbTx, tx} newBlock := bc.MineBlock(txs) fmt.Println("Success!") }
在我们的实现中,创建交易的人挖出了新的区块,得到奖励。
UTXO Set
在第三章持久化和命令行接口中,我们学习了比特币中存储区块到数据库的方式。文中提到区块被存放在blocks数据库,交易output存放在chainstate数据库中。这里说一下chainstate的结构:
1. ‘c’ + 32-byte transaction hash -> unspent transaction output record for that transaction
2. ‘B’ -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs
翻译一下
1. ‘c’ + 32-byte 交易的hash值 -> 未完成的交易记录
2. ‘B’ -> 32-byte 块hash值: 数据库记录的未使用的交易的output的块hash
第三篇文章里我们已经实现了交易,但是没有使用chainstate来保存他们的output,现在来实现这个。
chainstate不存放交易,相反,它保存UTXO(unspent transaction outputs,有结余交易的output)集合。除此之外,它保存“数据库记录的未使用的交易的output的块hash”,我们会忽略这个特性,因为我们没有使用区块的高度(下一篇里会讨论实现)。
那为什么我要有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 } } ... }
该函数负责把有未消费完output的交易找出来。因为交易是存放在区块中的,所以这个方法会迭代所有的区块链中的区块,并检测区块中的每一个交易。到2017年9月18号,比特币中已经有485860个区块,而全部的数据用了140+GB的磁盘空间。这意为着如果要验证交易,则要检测所有的节点。而且,验证交易将需要遍历很多区块。
而解决方案是要给未消费完的output建立索引,这就是UTXO的作用:这个缓存是基于所有区块链中交易(通过遍历了所有的区块,当然了,只执行了一次)创建的,然后就用来计算余额和验证新的交易。这个UTXO的大小在2017年9月大概是有2.5Gb。
好了,我们要想一下要如何改造UTXO的实现方法。当前,下面这些方法是用于查找交易的:
1. Blockchain.FindUnspentTransactions 找到所有含有未消费output的交易主函数。遍历所有的区块在该函数里执行。
2. Blockchain.FindSpendableOutputs 当有新的交易创建时使用。如果找足够交易所需数的output。会调用Blockchain.FindUnspentTransactions方法
3. Blockchain.FindUTXO 找到未消费的output来创建公钥hash,调用 Blockchain.FindUnspentTransactions方法。
4. Blockchain.FindTransaction 通过交易的ID在区块链中找到交易。它会遍历所有区块直到找到该交易。
可以看到,这些方法遍历了整个数据库中的所有区块。但是现在我们不能改善这些方法,因为UTXO集合没有存放在所有的交易,而只有那些含未消费output的。因此,还不能在Blockchain.FindTransaction使用。
所以我们需要下面的这些方法:
1. Blockchain.FindUTXO 通过遍历所有区块找到所有未消费的output
2. UTXOSet.Reindex 调用FindUTXO来查找所有没有消费的output,然后存储到数据库中。这里是缓存执行的地方。
3. UTXOSet.FindSpendableOutputs 类似Blockchain.FindSpendableOutputs,但是使用的是UTXO集合。
4. UTXOSet.FindUTXO 类似Blockchain.FindUTXO,但是使用的是UTXO集合。
5. Blockchain.FindTransaction 保持不变
因此,用得最多的两个方法从现在开始就会使用缓存,开始码代码:
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集合。首先移除所有的存在的桶,然后从区块链中找到所有未消费的output,最后把这些output存到桶中去。
Blockchain.FindUTXO几乎与Blockchain.FindUnspentTransactions是相同的,但是它返回的是TransactionID → TransactionOutputs映射组合的map。
现在,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相应版本的方法相比,有些轻微的改动。而那些Blockchain中对应的方法就没有用了。
用了UTXO集合我们(交易的)数据就可以分开存放了:实际的交易存放在区块链中,未消费的output则存放在UTXO集合里。这样的分离需要坚固的同步机制,因为我们得让UTXO集合总是能更新和保存所有最近交易的output。但是我们不需要每次新区块挖出来时重排索引,因为我们要避免频繁的区块链查找。因此,需要一个机制来更新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集合就会被更新。更新意味着会清除掉被消费了的output,及增加新挖出的交易中未消费的output。如果一笔交易的output被移除了,内部也没有其它output时,它也会除掉。相当简单。
在必要的地方使用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集合就会被更新。
检测一下是否工作:
blockchain g ocreateblockchain − address 1 JnMDSqVoHi 4 TEFXNw 5 wJ 8 skPsPf 4 LHkQ 100000086 a 725 e 18 ed 7 e 9 e 06 f 1051651 a 4 fc 46 a 315 a 9 d 298 e 59 e 57 aeacbe 0 bf 73 Done ! blockchaingocreateblockchain−address1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ100000086a725e18ed7e9e06f1051651a4fc46a315a9d298e59e57aeacbe0bf73Done! 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地址收到三个奖励:
1. 挖出创世区块的奖励
2. 挖出0000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21b区块
3. 挖出000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433区块
默克尔树
在这里要再多讨论一个的优化机制。
前面说到,完整的区块链数据库(即区块链)花掉了140Gb的磁盘存储空间。因为去中心化的特性,每个在网络中的节点都必须独立且足够自主,也即每个节点都必须保存整个区块链的副本。随着人们开始使用比特币,这一规则就会变得困难,每个人都要运行所有节点显然是不合适的。还有,因为节点都是网络中完全成熟的部分,它们都有责任:必须验证交易和区块。另外,得能连上网络与其它节点交互和下载新的区块。
在中本始发表的最初的比特币论文中,已经有一个解决方案来处理这一问题,简化支付验证(Simplified Payment Verification,SPV)。SPV是一个轻量的比特币节点,不会下载整个区块连,也不验证区块和交易。相反,它会找出区块(用于验证交易)中的交易,且和所有的节点连接来检索必要的数据。这一机制允许运行多个轻量的钱包节点和只需一个全量节点。
为了实现SPV的可行性,得有一种方式能检测在不下载整个区块的情况下,判断该区块包含了指定的交易。为了解决这个问题,需要引入默克尔树。
默克尔树被用于比特币来获取交易hash值,该hash存放在block的头部以及在工作证明中会被用到。直到现在,我们也只是把区块中的每一个交易hash串起来,再用SHA-256计算它们。这当然也是获取唯一的区块中的交易描述的好方式,但是并没有默克尔树的优点。
看看默克尔树:
默克尔树为了每一个区块而创建,开始于叶(树的底部)节点,叶子就是一个交易的hash值(比特币使用两次SHA256计算)。叶子的数量必须是偶数的,但是并不是每一个区块都含有偶数个交易。如果有奇数个交易,最后一个交易就会重复(在默克尔树里是这样,不是区块中!)。
从底而上,叶子被分成组成一对,它们的hash是串起来的,并且串起来的hash也会生成新的hash。新的hash生成新的树节点。这一过程会一直持续直到只剩下一个节点,也就是树的根节点。根节点的hash就会被当成这些交易的描述存放在区块的头部,然后在工作量证明中会用到。
使用默克尔树的好处就是节点可以清楚与指定交易的关系,而不需要下载整个区块。只需要一个交易hash,默克尔树的根节点hash,还有树的路径即可。
开始撸代码:
type MerkleTree struct { RootNode *MerkleNode } type MerkleNode struct { Left *MerkleNode Right *MerkleNode Data []byte }
从结构开始,每一个MerkleNode都会持有数据和连接到它的分支。MerkleTree实际上就是和后面的节点连接的根节点,而它们与其它节点连接,等等。
创建新的节点:
func NewMerkleNode(left, right *MerkleNode, data []byte) *MerkleNode { mNode := MerkleNode{} if left == nil && right == nil { hash := sha256.Sum256(data) mNode.Data = hash[:] } else { prevHashes := append(left.Data, right.Data...) hash := sha256.Sum256(prevHashes) mNode.Data = hash[:] } mNode.Left = left mNode.Right = right return &mNode }
每一个节点包含了一些数据。当节点是叶子节点时,数据来自外方(从我们的角度看就是序列化的交易)。当节点连接到其它(左右)节点时,它就会把这左右两个节点的数据串起来,然后计算串起来的hash值作为自己的数据。
func NewMerkleTree(data [][]byte) *MerkleTree { var nodes []MerkleNode if len(data)%2 != 0 { data = append(data, data[len(data)-1]) } for _, datum := range data { node := NewMerkleNode(nil, nil, datum) nodes = append(nodes, *node) } for i := 0; i < len(data)/2; i++ { var newLevel []MerkleNode for j := 0; j < len(nodes); j += 2 { node := NewMerkleNode(&nodes[j], &nodes[j+1], nil) newLevel = append(newLevel, *node) } nodes = newLevel } mTree := MerkleTree{&nodes[0]} return &mTree }
当新的树创建好时,第一件事就是确定是否有偶数个叶子。然后,数据(data,交易的序列化数组)会被转换成树叶,并且新树会基于这些叶子长出来。
现在修改Block.HashTransactions,它的作用是在工作量证明时获取交易的hash:
func (b *Block) HashTransactions() []byte { var transactions [][]byte for _, tx := range b.Transactions { transactions = append(transactions, tx.Serialize()) } mTree := NewMerkleTree(transactions) return mTree.RootNode.Data }
首先,区块中的交易会串起来,然后被序列化(使用encoding/gob),然后用于创建新的默克尔树。该树的根节点会充当该区块的所有交易的标识。
P2PKH
还有一个问题在这里讨论一下:
我们说过,比特币中使用了Script脚本编程语言,它被用在给交易的output加锁,然后交易的input提供数据来解锁output。这个语言很简单,代码也只是一串序列和一些操作符。
看这个例子:
5 2 OP_ADD 7 OP_EQUAL
5,2,和7是数据。OP_ADD和OP_EQUAL就是操作符。Script代码是可以从左到右被执行的,每一片代码被放到栈中,然后下一个操作符就会用于栈顶的元素。Script栈仅是简单的FILO方式使用内存,第一个元素进栈会最后一个出栈,后面的元素会被放到前一个的上面。
我们拆开上面的代码来逐步分析:
序号 栈 脚本 1 空 5 2 OP_ADD 7 OP_EQUAL 2 5 2 OP_ADD 7 OP_EQUAL 3 5 2 OP_ADD 7 OP_EQUAL 4 7 7 OP_EQUAL 5 7 7 OP_EQUAL 6 true empty
操作OP_ADD就是从栈顶先后取出两个元素相加,然后把结果压到栈顶。OP_EQUAL同样从栈顶取出两个元素判断相等,把结果压入栈顶。当脚本执行完时,栈顶的元素就是该脚本的执行结果:在我们的这个例子中,结果就是true,也即是说脚本成功执行完成。
现在看看比特币中用于支付的脚本:
OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG
这种叫Pay to Public Key Hash(P2PKH),是比特币中用得最广泛的脚本。它会逐个支付给公钥的hash,即会锁住指定公钥下的币。这是比特币的支付核心:没有账户,彼此之间没有现金交换;也仅是有一段脚本来检测提供的签名和公钥是否正确。
这段脚本实际上保存有两个部分:
1. 第一段: 存放了input的ScriptSig栏位。
2. 第二段:OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG 存放output的 ScriptPubKey。
因此,这段脚本定义了解锁的逻辑,就是input提供了数据来解锁output。执行这段脚本:
序号 栈 脚本 1 空 <signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG 2 <signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG 3 <signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG 4 <signature> <pubKey> <pubKey> OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG 5 <signature> <pubKey> <pubKeyHash> <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG 6 <signature> <pubKey> <pubKeyHash> <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG 7 <signature> <pubKey> OP_CHECKSIG 8 true 或 false 空
OP_DUP操作会复制栈顶的元素。OP_HASH160获取栈顶的元素并使用(RIPEMD160)算法计算其hash值,把结果压到栈顶。OP_EQUALVERIFY比较栈顶的两个元素,如果不等,则打断脚本。OP_CHECKSIG通过计算交易的hash和使用及验证交易的签名。后面的操作比较复杂:使用一个被修整过的交易副本,计算它的hash(因为它就是一个被签名了的交易的hash),然后用提供的及检测这个签名是否正确。
拥有脚本语言的使得比特币可能成为智能合约平台:这个语言除了能支持每次交易都使用单一的密钥转移比特币,其它的支付场景也成为可能。
总结
好了,我们实现了大多数基于区块链加密货币的差关键特性。区块链、地址、挖矿、交易。但是,还有一个赋予这些特性生命的机制,创造了比特币的全局系统,一致性。下一章我们开始实现区块链的一部分–“去中心化”。敬请期待!
-
学院 Go 语言视频主页
https://edu.csdn.net/lecturer/1928 -
扫码获取海量视频及源码 QQ群:721929980
以上所述就是小编给大家介绍的《2.7 交易确认》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Bytom的链式交易和花费未确认的交易
- BCH网络Avalanche概念验证成果喜人,交易确认速度达ETH 10倍
- RabbitMQ消息确认机制+延时队列
- 华为确认存在备用操作系统
- 需求确认的方法和内容,了解一下
- RabbitMQ的消息确认机制那些事
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
图片转BASE64编码
在线图片转Base64编码工具
XML 在线格式化
在线 XML 格式化压缩工具