首页 > 世链号 > 比特币的核心技术开发之交易2(上)
区块资讯室  

比特币的核心技术开发之交易2(上)

摘要:在这个系列文章的一开始,我们就提到了,区块链是一个分布式数据库。不过在之前的文章中,我们选择性地跳过了“分布式”这个部分,而是将注意力都放到了“数据库”部分。

[区块链研究实验室]比特币的核心技术开发之交易2(上)

前言

在这个系列文章的一开始,我们就提到了,区块链是一个分布式数据库。不过在之前的文章中,我们选择性地跳过了“分布式”这个部分,而是将注意力都放到了“数据库”部分。到目前为止,我们几乎已经实现了一个区块链数据库的所有元素。今天,我们将会分析之前跳过的一些机制。而在下一篇文章中,我们将会开始讨论区块链的分布式特性。

挖矿的奖励

在上一篇文章中,我们略过的一个小细节是挖矿奖励。现在,我们已经可以来完善这个细节了。

挖矿奖励,实际上就是一笔 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集

在 Part 3: 持久化和命令行接口 中,我们研究了 Bitcoin Core 是如何在一个数据库中存储块的,并且了解到区块被存储在 blocks 数据库,交易输出被存储在 chainstate 数据库。会回顾一下 chainstate 的机构:

  1. c + 32 字节的交易哈希 -> 该笔交易的未花费交易输出记录

  2. 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) ==  {

            break

        }

    }

    ...}

这个函数找到有未花费输出的交易。由于交易被保存在区块中,所以它会对区块链里面的每一个区块进行迭代,检查里面的每一笔交易。截止 217 年 9 月 18 日,在比特币中已经有 485,86 个块,整个数据库所需磁盘空间超过 14 Gb。这意味着一个人如果想要验证交易,必须要运行一个全节点。此外,验证交易将会需要在许多块上进行迭代。

整个问题的解决方案是有一个仅有未花费输出的索引,这就是 UTXO 集要做的事情:这是一个从所有区块链交易中构建(对区块进行迭代,但是只须做一次)而来的缓存,然后用它来计算余额和验证新的交易。截止 217 年 9 月,UTXO 集大概有 2.7 Gb。

好了,让我们来想一下实现 UTXO 集的话需要作出哪些改变。目前,找到交易用到了以下一些方法:

  1. Blockchain.FindUnspentTransactions - 找到有未花费输出交易的主要函数。也是在这个函数里面会对所有区块进行迭代。

  2. Blockchain.FindSpendableOutputs - 这个函数用于当一个新的交易创建的时候。如果找到有所需数量的输出。使用 Blockchain.FindUnspentTransactions.

  3. Blockchain.FindUTXO - 找到一个公钥哈希的未花费输出,然后用来获取余额。使用 Blockchain.FindUnspentTransactions.

  4. Blockchain.FindTransation - 根据 ID 在区块链中找到一笔交易。它会在所有块上进行迭代直到找到它。

可以看到,所有方法都对数据库中的所有块进行迭代。但是目前我们还没有改进所有方法,因为 UTXO 集没法存储所有交易,只会存储那些有未花费输出的交易。因此,它无法用于 Blockchain.FindTransaction。

所以,我们想要以下方法:

  1. Blockchain.FindUTXO - 通过对区块进行迭代找到所有未花费输出。

  2. UTXOSet.Reindex - 使用 UTXO 找到未花费输出,然后在数据库中进行存储。这里就是缓存的地方。

  3. UTXOSet.FindSpendableOutputs - 类似 Blockchain.FindSpendableOutputs,但是使用 UTXO 集。

  4. UTXOSet.FindUTXO - 类似 Blockchain.FindUTXO,但是使用 UTXO 集。

  5. Blockchain.FindTransaction 跟之前一样。

因此,从现在开始,两个最常用的函数将会使用 cache!来开始写代码吧。

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 集。首先,如果 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 :=

    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 集:

本文来源:区块链研究实验室

免责声明
世链财经作为开放的信息发布平台,所有资讯仅代表作者个人观点,与世链财经无关。如文章、图片、音频或视频出现侵权、违规及其他不当言论,请提供相关材料,发送到:2785592653@qq.com。
风险提示:本站所提供的资讯不代表任何投资暗示。投资有风险,入市须谨慎。
世链粉丝群:提供最新热点新闻,空投糖果、红包等福利,微信:juu3644。