最近事情比较多,这一篇现在才更。又开始忙起来了~在本篇文章所涉及到的代码实现中,大部分改动相比之前的在条理和逻辑上更加清晰。
引言
在本系列文章中的最开始,我说过区块链是一个分布式数据库。那时,我们决定先跳过“分布式”的部分而把精力放到“数据库”相关的内容。不久之前,我们几乎已经实现了所有的关于数据库部分的内容。在这篇文章中,会覆盖到先前我们跳过的一些机制,同时在下一篇文章中,我们会开始着手于区块链的分布式特性。
先前的章节:
(原文地址可能无法访问)
本部分只介绍有重大变化的代码,所以在这里将它们全部进行解释是没有必要的。请根据这个页面去看代码的变动(与上一篇文章比较)。
奖励
在先前的文章中,我们跳过了一个很细微的细节就是挖矿奖励。现在我们具有了实现它的一切。
这个奖励是仅仅是一个coinbase
交易。当一个挖矿节点开始挖一个新的区块时,它收集队列中的交易同时为它们准备一个coinbase
交易。这个coinbase
交易仅仅包含一个矿工公钥哈希的输出。
实现奖励和更新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 集合
在第三部分持久化和命令行接口中,我们学习了比特币内核将区块存储到数据库的方式。它讲到区块都是存储在blocks
数据库的,交易出账是存储在chainstate
数据库的。让我提醒你一下chainstate
的结构是什么样的:
- ‘c’ + 32位交易哈希 -> 交易的未花费交易输出记录
- ‘B’ + 32位区块哈希 -> 数据库中代表未花费交易输出的区块哈希
至这一篇文章,我们已经实现了交易,但是我们并没有去使用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.7Gb。
好的,让我们想想我们实现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
和之前的一样。
因此,最常使用的两个函数将从现在起使用到高速缓存!
让我们开始coding!
type UTXOSet struct {
Blockchain *Blockchain
}
我们将会使用一个单独的数据库,但是我们将会把UTXO集合存储到一个不同的bucket
里。因此,UTXO
是和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集合。首先,如果它存在一个bucket
的话讲首先将它移除,然后从区块链上寻找所有的未花费交易输出,最后将这些出账存储到bucket
里。
Blockchain.FindUTXO
方法几乎和Blockchain.FindUnspentTransactions
是相同的,但是现在它返回一个TransactionID
到TransactionOutputs
的Map
(映射)结构。
现在,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集合在新的区块被挖出后更新:
让我们检查一下它的工作:
$ 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
地址接受了三次奖励:
- 一次来自挖出创世纪块。
- 一次来自挖出区块
0000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21b
。 - 一次来自挖出区块
000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433
。
Merkle Tree(默克尔树)
这里有一个最好的机制我想在这部分讨论一下。
正如我们上文所说的,全部的比特币数据库(或者说区块链)使用了超过140Gb的磁盘空间。由于比特币去中心化的特性,网络中的每个节点必须独立和自给自足,也就是说每个节点必须存储整个区块链的副本。随着更多的人开始使用比特币,这个规则变得很难去遵守:没有可能每个人都去运行全节点。同时,由于节点是网络的成熟的参与者,它们拥有责任:它们必须验证交易和区块。同时,它们需要有确切的互联网流量来与其他节点进行交互并且下载新的区块。
在中本聪发布的比特币原始论文中,对这个问题有一个解决方案:简单支付验证(Simplified Payment Verification SPV)。SPV是一个轻节点,不需要下载全部的区块链也不需要对区块和交易进行验证。取而代之,它在区块中寻找交易(用来验证支付)同时连接到一个全节点检索必要的数据(也就是说轻节点需要某些数据时,可以从连接全节点,在上面下载)。这个机制允许多个轻钱包节点只运行一个全节点。
为了让SPV成为可能,这就应该有一种检查一个区块是否包含特定的交易而不用下载整个区块的方法。这就是Merkle树所做的。
Merkle树被比特币用来获取交易的哈希,也就是之后存储在区块头并且被工作量证明所考虑到。直到现在,我们仅仅连接了区块中每个交易的哈希然后对它们使用SHA-256
方法。这也是一个很好的方式来获取区块交易的独一无二的特征值,但是没有Merkle树的优势。
让我们看看Merkle树:
一个Merkle树是为每一个区块建立的,它从叶节点开始(树的底部),一个叶子是一个交易哈希(比特币使用双重SHA256
哈希算法)。叶节点的数目比必须是偶数,但是并不是每一个区块都包含偶数个数的交易。如果一个区块的交易数是奇数,那么最后一个交易将会被复制(在Merkle树中,不是区块中!)。
离开底部,叶节点成对成组,它们的哈希被连接,同时一个新的哈希从它们相连的哈希得到。新的哈希组成新的树节点。这个步骤被重复执行直至仅剩一个节点,叫做树的根节点。这个根哈希被用做这些交易的独一无二的特征代表,被存储在区块头中,用于工作量证明程序。
最后,写代码:
type MerkleTree struct {
RootNode *MerkleNode
}
type MerkleNode struct {
Left *MerkleNode
Right *MerkleNode
Data []byte
}
我们从结构体开始。每一个MerkleNode
(Merkle节点)维持数据并且连接到分支上。MerkleNode
是真正的连接到下一个节点的根节点,又连接到更远的节点。
让我们首先创建一个节点:
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
}
每个节点包含一些数据。当一个节点是叶子时,数据在外界获得(我们的实例中是序列化的交易)。当一个节点连接到另一个节点时,它获取它们的数据并且连接它们然后进行哈希运算。
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
}
当一个新的树被创建,第一件事就是确定是否是偶数个叶子。在那之后,数据(一个序列化交易数组)被传入树叶子,然后树开始由这些叶子生长。
现在,让我们修改Block.HashTransactions
,在工作量证明中被使用来获取交易哈希:
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
),然后它们被用来构建Merkle树。树的根节点被作为这些区块交易的独一无二的标识来存储。
P2KH
这部分相当于扩展内容,有关比特币的脚本语言。我好困好想睡~就先转AnnatarHe的了。
在细节上还有一点要说一下。
你记得吗,在比特币种有一种脚本(Script)编程语言,它被用来锁定交易出账:交易入账提供数据去锁定出账。这个语言非常简单,语言的代码也就仅仅是数据和操作符的排列而已。看下这个例子:
5 2 OP_ADD 7 OP_EQUAL
5, 2, 和 7 都是数据. OP_ADD 和 OP_EQUAL 是操作符。Script 的代码是从左至右执行的:数据的每一块都被塞进栈里然后下个操作会会被栈顶的元素调用。Script的栈只是一个简单的 FILO(先入后出)内存存储:栈中的第一个进去的元素会被最后一个拿走,之后进来的每个元素都是放到前一个的上面。
来分解一下上面这个脚本执行的步骤吧:
- 栈:空。脚本:5 2 OP_ADD 7 OP_EQUAL
- 栈:5。脚本:2 OP_ADD 7 OP_EQUAL
- 栈:5 2。脚本:OP_ADD 7 OP_EQUAL
- 栈:7。脚本:7 OP_EQUAL
- 栈:7 7。脚本:OP_EQUAL
- 栈:true。脚本:空
OP_ADD拿走栈上的两个元素,求和,然后把和再塞进栈里。OP_EQUAL从栈里拿两个元素,然后比较: 如果一样就把true 推到栈里,不一样就把false推进去。脚本执行的结果就是栈顶的值:在我们的场景下,它是 true,这就意味着脚本正常地成功执行了。
现在来看一眼比特币中执行支付的脚本:
<signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
这个脚本被称作付款给公钥哈希(Pay to Public Key Hash)(P2PKH),这是比特币中最常用的脚本。它就是字面上的给公钥哈希付款的意思,它会用一个确定的公钥锁币。这是 比特币支付的核心:无账户,两者之间无资金交互;只有脚本去确认提供的数字签名和公钥是正确的。
此脚本实质上存在两个部分:
- 第一块。signature, pubKey存在入账的 ScriptSig 字段中。
- 第二部分。OP_DUP OP_HASH160 pubKeyHash OP_EQUALVERIFY OP_CHECKSIG存在出账的 ScriptPubKey 中。
所以,它是定义解锁逻辑的出账,也是提供数据区解锁出账的入账。来执行以下这个脚本:
1 栈: empty 脚本: 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. Script: empty.
OP_DUP 复制栈顶的一个元素. OP_HASH160 拿走栈顶的元素,并用 RIPEMD160 哈希一下; 再把结果塞到栈里. OP_EQUALVERIFY 对比栈顶的两个元素,如果不一样就中断脚本的执行. OP_CHECKSIG 通过哈希交易,还有 signature 和 pubKey 来验证交易的签名. 后面的一个操作颇为复杂: 它做了一个简版的交易副本, 对它哈希(因为这是被签名的交易哈希), 然后用提供的 signature 和 pubKey 验证签名.
有了这样的脚本语言就允许比特币可以成为智能合约平台:这种语言是的除了穿衣单个秘钥之外的其他交易方式成为了可能。
总结
好啦!我们几乎实现了以区块链为基础的加密货币的所有关键特性。我们有了区块链、地址、挖矿以及交易。但是还有一件事赋予这些所有机制生命,使区块链成为一个全局生态:一致性。在下一篇文章中,我们将开始实现区块链的“去中心化”部分。尽请期待!
Links: