用Go構建區塊鏈——6.交易2

weixin_34107955發表於2018-06-26

用Go構建區塊鏈——6.交易2

本篇是"用Go構建區塊鏈"系列的第六篇,主要對原文進行翻譯。對應原文如下:

Building Blockchain in Go. Part 6: Transactions 2

1、介紹

在本系列的第一部分中,我說過區塊鏈是一個分散式資料庫。那時候,我們決定跳過"分散式"部分,專注於"資料庫"部分。到目前為止,我們已經實現了幾乎所有構成區塊鏈資料庫的東西。在本文中,我們將介紹一些在前面部分中跳過的機制,下一部分我們將開始研究區塊鏈的分散式特性。

之前的章節:

  1. 基本原型
  2. 工作量證明
  3. 持久化和命令列
  4. 交易1
  5. 地址

這部分介紹了重要的程式碼更改,所以在這裡解釋它們是沒有意義的。請參閱此頁面以檢視自上一篇文章以來的所有更改。

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!")
}

在我們的實現中,建立交易的人挖到新區塊時,會得到一筆獎勵。

3、UTXO 集

第三部分:持久化和命令列 中,我們研究了比特幣核心在資料庫中儲存塊的方式。據說塊儲存在 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) == 0 {
            break
        }
    }
    ...
}

這個方法會找到那些未花費輸出的交易。由於交易被儲存在區塊中,所以它會遍歷區塊鏈上的每個區塊,來檢查每筆交易。截至2017年9月18日,比特幣中有485,860個塊,整個資料庫需要140 Gb以上的磁碟空間。這意味著必須執行一個完整節點來驗證交易。而且,驗證交易將需要遍歷許多區塊。

這個問題的解決方案是有一個只儲存未花費輸出的索引,這就是UTXO集合所做的:這是一個由所有區塊鏈交易構建的快取(通過迭代塊,是的,但是這隻能完成一次),並且稍後用於計算餘額並驗證新的交易。截至2017年9月,UTXO集合約為2.7 Gb的磁碟空間。

好的,讓我們想想我們需要改變以實現UTXO集。目前,以下方法用於查詢交易:

  1. Blockchain.FindUnspentTransactions - 查詢未花費輸出的交易的主要功能。這是所有區塊迭代發生的這個函式。
  2. Blockchain.FindSpendableOutputs - 建立新交易時使用此功能。如果找到足夠數量的輸出持有所需的數量。使用 Blockchain.FindUnspentTransactions
  3. Blockchain.FindUTXO - 找到公密雜湊的未花費輸出,用於獲取餘額。使用 Blockchain.FindUnspentTransactions
  4. Blockchain.FindTransaction - 通過ID在區塊鏈中查詢交易。它遍歷所有區塊直到找到為止。

你可以看到,所有的方法都要遍歷資料庫中的區塊。但現在我們無法改進它們,因為UTXO集不儲存所有事務,但只儲存那些沒有使用輸出的事務。因此,它不能用於 Blockchain.FindTransaction

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

  1. Blockchain.FindUTXO - 通過迭代區塊來查詢所有未花費的輸出。
  2. UTXOSet.Reindex - 用於FindUTXO查詢未使用的輸出,並將它們儲存在資料庫中。這就是快取發生的地方。
  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集。首先,如果已經存在了bucket,就把它刪掉。然後從區塊鏈中獲取所有未花費的輸出,最後將輸出儲存到儲存桶(bucket)中。

Blockchain.FindUTXO 幾乎跟 Blockchain.FindUnspentTransactions 完全相同,但現在它返回一 TransactionIDTransactionOutputs 的對映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 這個地址收到了3次獎勵:

  1. 一次來自挖掘出創世區塊。
  2. 一次來自挖掘出區塊 0000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21b
  3. 一次來自挖掘出區塊 000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433

4、Merkle樹

還有一個我想在這篇文章中討論的優化機制。

如上所述,完整的比特幣資料庫(即區塊鏈)需要超過140 Gb的磁碟空間。由於比特幣的去中心化特性,網路中的每個節點都必須是獨立的,即每個節點都必須儲存完整的區塊鏈副本。隨著越來越多的人開始使用比特幣,這條規則變得更加難以遵循:每個人都不可能執行一個完整的節點。另外,由於節點是網路的完整參與者,他們有責任:他們必須驗證交易和區塊。此外,需要有一定的網際網路流量來和其他節點互動,下載新的區塊。

在中本聰公佈的比特幣原始論文中,針對這種情況有一種解決方案:簡單支付驗證(Simplified Payment Verification, SPV)。SPV 是一個比特幣的輕量節點,這種節點不會下載整個區塊鏈,也不會驗證區塊和交易。而是找到區塊中的交易(以驗證支付)然後連結到完整節點以檢索必要的資料。這種機制允許多個輕量錢包節點只執行一個完整的節點。

為了使SPV成為可能,應該有一種方法來檢查一個區塊是否包含某個交易而不下載整個區塊。Merkle樹就是在這裡發揮作用的。

比特幣使用Merkle樹來獲取交易雜湊,然後將其儲存在區塊頭中,並會用於工作量證明系統。到目前為止,我們只是將一個區塊裡面的每筆交易雜湊連線了起來,其中應用了 SHA-256 演算法。這也是一種獲取區塊交易唯一表示的一種好方法,但是它並沒有 Merkle 樹的優勢。

我們來看一下Merkle樹:


5521305-e9686dc1e41a1f4a.png
Merkle樹

Merkle樹是為每個區塊構建的,它以葉子(樹的底部)開始,葉子是一個交易雜湊(比特幣使用兩次 SHA256 雜湊)。葉子的數量必須是偶數,但不是每個區塊都包含偶數個交易。如果存在奇數個交易,則最後一個交易將被複制(只是在Merkle樹中,而不是在區塊鏈中!)。

從底部開始,葉子節點會成對分組,它們的雜湊會連線在一起,並從連線的雜湊中獲取新的雜湊。新雜湊形成新的樹節點。重複這個過程,直到只有一個節點被稱為樹的根節點。然後將根雜湊用作交易的唯一表示,儲存在區塊頭中,並用於工作量證明系統中。

Merkle樹的好處是節點可以在不下載整個區塊的情況下,驗證是否包含某筆交易。為此,只需要一個交易雜湊,一個Merkle樹的根雜湊和一個Merkle路徑。

最後,讓我們編寫程式碼:

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
}

每個節點都包含一些資料。當一個節點是葉子節點時,資料從外界傳遞(在我們的例子中是一個序列化的交易)。當一個節點連結到其他節點時,它會將其他節點的資料取過來,連線後再雜湊。

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樹。樹的根節點將會被用作區塊交易的獨特 ID。

5、P2PKH

還有一件事,我想更詳細地討論。

正如你記得的,在比特幣中有指令碼(Script)程式語言,它用於鎖定交易輸出; 並且交易輸入提供資料來解鎖輸出。語言很簡單,這種語言的程式碼只是一系列資料和操作符。考慮這個例子:

5 2 OP_ADD 7 OP_EQUAL

5 , 2, 和 7 是資料,OP_ADDOP_EQUAL 是操作符。指令碼程式碼從左到右執行:每個資料都被放入堆疊,下一個操作符被應用到頂層堆疊元素。指令碼的堆疊只是一個簡單的FILO(First Input Last Output)儲存器:堆疊中的第一個元素是最後一個元素,每個元素都放在前一個元素上。

讓我們將上述指令碼的執行分解成以下幾個步驟:

  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。指令碼:空

OP_ADD從堆疊中獲取兩個元素,求和,並將總和推入堆疊。OP_EQUAL從堆疊中獲取兩個元素並對它們進行比較:如果它們相等,則將它推true入堆疊; 否則它推入false。指令碼執行的結果是頂層堆疊元素的值:在我們的例子中true,這意味著指令碼成功完成。

現在讓我們看一下比特幣用於執行支付的指令碼:

<signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

這個指令碼被稱為Pay to Public Key Hash(P2PKH),這是比特幣中最常用的指令碼。它從字面上支付公共金鑰雜湊值,即用某個公鑰鎖定硬幣。這是比特幣支付的核心:沒有賬戶,沒有資金在它們之間轉移; 只有一個指令碼檢查提供的簽名和公鑰是否正確。

該指令碼實際上儲存在兩部分中:

  1. 第一部分,<signature> <pubKey>儲存在輸入ScriptSig欄位中。
  2. 第二部分,OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG儲存在輸出中ScriptPubKey。

因此,它是定義解鎖邏輯的輸出,並且它是提供資料以解鎖輸出的輸入。讓我們執行指令碼:

  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

  1. 堆疊:<signature> <pubKey>
    指令碼:OP_CHECKSIG

  2. 堆疊:true或false。指令碼:空。

OP_DUP 複製頂層堆疊元素。OP_HASH160 取頂部堆疊元素並將用 RIPEMD160 雜湊計算; 結果被推回到堆疊。OP_EQUALVERIFY 比較兩個頂層堆疊元素,如果它們不相等,則會中斷指令碼。OP_CHECKSIG 通過雜湊交易並使用 <signature> 和驗證交易的簽名 <pubKey> 。後者的運算子相當複雜:它做了一個簡單的交易副本, 對它雜湊(因為這是被簽名的交易雜湊), 然後用提供的 signaturepubKey 驗證簽名.

擁有這樣的指令碼語言可以讓比特幣成為一個智慧合約平臺:除了轉移到一個單一的金鑰之外,該語言還可以實現其他支付方案。

6、總結

就是這樣!我們已經實現了幾乎所有基於區塊鏈的加密貨幣的關鍵功能。我們擁有區塊鏈,地址,採礦和交易。但還有一件事讓所有這些機制生機勃勃,並使比特幣成為全球系統:共識。在下一篇文章中,我們將開始實現區塊鏈的"去中心化"部分。敬請關注!

連結:

  1. 完整的原始碼
  2. UTXO集
  3. Merkle樹
  4. 指令碼
  5. “Ultraprune”比特幣核心提交
  6. UTXO集統計
  7. 智慧合約和比特幣
  8. 為何每個比特幣使用者都應該瞭解"SPV安全"

由於水平有限,翻譯質量不太好,歡迎大家拍磚。

相關文章