2.7 交易确认

栏目: 数据库 · 发布时间: 6年前

内容简介:有一件前面的章节中跳过了一个小细节,挖矿奖励。现在我们准备实现这个。这个奖励也就是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计算它们。这当然也是获取唯一的区块中的交易描述的好方式,但是并没有默克尔树的优点。 

看看默克尔树: 

2.7 交易确认

默克尔树为了每一个区块而创建,开始于叶(树的底部)节点,叶子就是一个交易的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),然后用提供的及检测这个签名是否正确。 

拥有脚本语言的使得比特币可能成为智能合约平台:这个语言除了能支持每次交易都使用单一的密钥转移比特币,其它的支付场景也成为可能。

总结

好了,我们实现了大多数基于区块链加密货币的差关键特性。区块链、地址、挖矿、交易。但是,还有一个赋予这些特性生命的机制,创造了比特币的全局系统,一致性。下一章我们开始实现区块链的一部分–“去中心化”。敬请期待!

2.7 交易确认


以上所述就是小编给大家介绍的《2.7 交易确认》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Mastering Bitcoin

Mastering Bitcoin

Andreas M. Antonopoulos / O'Reilly Media / 2014-12-20 / USD 34.99

Mastering Bitcoin tells you everything you need to know about joining one of the most exciting revolutions since the invention of the web: digital money. Bitcoin is the first successful digital curren......一起来看看 《Mastering Bitcoin》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具