用 Go 構建一個區塊鏈 -- Part 6: 交易(2)

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

翻譯的系列文章我已經放到了 GitHub 上:blockchain-tutorial,後續如有更新都會在 GitHub 上,可能就不在這裡同步了。如果想直接運行代碼,也可以 clone GitHub 上的教程倉庫,進入 src 目錄執行 make 即可。

引言

在這個系列文章的一開始,我們就提到了,區塊鏈是一個分散式資料庫。不過在之前的文章中,我們選擇性地跳過了“分布式”這個部分,而是將注意力都放到了“資料庫”部分。到目前為止,我們幾乎已經實現了一個區塊鏈資料庫的所有元素。今天,我們將會分析之前跳過的一些機制。而在下一篇文章中,我們將會開始討論區塊鏈的分布式特性。

之前的系列文章:

  1. 基本原型

  2. 工作量證明

  3. 持久化和命令列介面

  4. 交易(1)

  5. 地址

本文的代碼實現變化很大,請點擊 這裡 查看所有的代碼更改。

獎勵

在上一篇文章中,我們略過的一個小細節是挖礦獎勵。現在,我們已經可以來完善這個細節了。

挖礦獎勵,實際上就是一筆 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) == 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.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 中。因此,UTXOSetBlockchain 一起。

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 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ100000086a725e18ed7e9e06f1051651a4fc46a315a9d298e59e57aeacbe0bf73Done!$ blockchain_go send -from 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1 -to 12DkLzLQ4B3gnQt62EPRJGZ38n3zF4Hzt5 -amount 60000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21bSuccess!$ blockchain_go send -from 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1 -to 12ncZhA5mFTTnTmHq1aTPYBri4jAK8TacL -amount 4000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433Success!$ blockchain_go getbalance -address 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1Balance of '1F4MbuqjcuJGymjcuYQMUVYB37AWKkSLif': 20$ blockchain_go getbalance -address 12DkLzLQ4B3gnQt62EPRJGZ38n3zF4Hzt5Balance of '1XWu6nitBWe6J6v6MXmd5rhdP7dZsExbx': 6$ blockchain_go getbalance -address 12ncZhA5mFTTnTmHq1aTPYBri4jAK8TacLBalance of '13UASQpCR8Nr41PojH8Bz4K6cmTCqweskL': 4

很好!1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1 地址接收到了 3 筆獎勵:

  1. 一次是挖出創世塊

  2. 一次是挖出塊 0000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21b

  3. 一個是挖出塊 000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433

Merkle 樹

在這篇文章中,我還想要再討論一個最佳化機制。

上如上面所提到的,完整的比特幣資料庫(也就是區塊鏈)需要超過 140 Gb 的磁碟空間。因為比特幣的去中心化特性,網路中的每個節點必須是獨立,自給自足的,也就是每個節點必須儲存一個區塊鏈的完整副本。隨著越來越多的人使用比特幣,這條規則變得越來越難以遵守:因為不太可能每個人都去運行一個全節點。並且,由於節點是網路中的完全參與者,它們負有相關責任:節點必須驗證交易和區塊。另外,要想與其他節點互動和下載新塊,也有一定的網路流量需求。

在中本聰的 比特幣原始論文 中,對這個問題也有一個解決方案:簡易支付驗證(Simplified Payment Verification, SPV)。SPV 是一個比特幣輕節點,它不需要下載整個區塊鏈,也不需要驗證區塊和交易。相反,它會在區塊鏈尋找交易(為了驗證支付),並且需要串連到一個全節點來檢索必要的資料。這個機制允許在僅運行一個全節點的情況下有多個輕錢包。

為了實現 SPV,需要有一個方式來檢查是否一個區塊包含了某筆交易,而無須下載整個區塊。這就是 Merkle 樹所要完成的事情。

比特幣用 Merkle 樹來擷取交易雜湊,雜湊被儲存在區塊頭中,並會用於工作量證明系統。到目前為止,我們只是將一個塊裡面的每筆交易雜湊串連了起來,將在上面應用了 SHA-256 演算法。雖然這是一個用於擷取區塊交易唯一表示的一個不錯的途徑,但是它沒有利用到 Merkle 樹。

來看一下 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),然後使用序列後的交易構建一個 Mekle 樹。樹根將會作為塊交易的唯一識別碼。

P2PKH

還有一件事情,我想要再談一談。

大家應該還記得,在比特幣中有一個 指令碼(Script)程式設計語言,它用於鎖定交易輸出;交易輸入提供瞭解鎖輸出的資料。這個語言非常簡單,用這個語言寫的代碼其實就是一系列資料和操作符而已。比如如下樣本:

5 2 OP_ADD 7 OP_EQUAL

5, 2, 和 7 是資料,OP_ADDOP_EQUAL 是操作符。指令碼代碼從左至右執行:將資料依次放入棧內,當遇到操作符時,就從棧內取出資料,並將操作符作用於資料,然後將結果作為棧頂元素。指令碼的棧,實際上就是一個先進後出的記憶體儲存:棧裡的第一個元素最後一個取出,後面的每一個元素都會放到前一個元素之上。

讓我們來對上面的指令碼分部執行:

步驟 指令碼 說明
1 5 2 OP_ADD 7 OP_EQUAL 一開始棧為空白
2 5 2 OP_ADD 7 OP_EQUAL 從指令碼裡面取出 5 放入棧上
3 5 2 OP_ADD 7 OP_EQUAL 從指令碼裡面取出 2 放入棧上
4 7 7 OP_EQUAL 遇到操作符 OP_ADD, 從棧裡取出兩個運算元 52,相加後將結果放回棧上
5 7 7 OP_EQUAL 從指令碼裡面取出 7 放到棧上
6 true 遇到操作符 OP_EQUAL,從棧裡取出兩個運算元並比較,將比較的結果放回棧內,指令碼執行完畢,為空白

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_EQUALVERYFY 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
7 <signature> <pubKey> OP_CHECKSIG
8 truefalse

OP_DUP 對棧頂元素進行複製。OP_HASH160 取棧頂元素,然後用 RIPEMD160 對它進行雜湊,再將結果送回到棧上。OP_EQUALVERIFY 將棧頂的兩個元素進行比較,如果它們不相等,終止指令碼。OP_CHECKSIG 通過對交易進行雜湊,並使用 <signature>pubKey 來驗證一筆交易的簽名。最後的操作符有點複雜:它產生了一個修剪後的交易副本,對它進行雜湊(因為它是一個被簽名後的交易雜湊),然後使用提供的 <signature>pubKey 檢查簽名是否正確。

有了一個這樣的指令碼語言,實際上也可以讓比特幣成為一個智能合約平台:除了將一個單一的公開金鑰轉移資金,這個語言還使得一些其他的支付方案成為可能。

總結

這就是今天的全部內容了!我們已經實現了一個基於區塊鏈的加密貨幣的幾乎所有關鍵特性。我們已經有了區塊鏈,地址,挖礦和交易。但是要想給這些所有的機制賦予生命,讓比特幣成為一個全球系統,還有一個不可或缺的環節:共識(consensus)。在下一篇文章中,我們將會開始實現區塊鏈的“去中心化(decenteralized)”。敬請收聽!

連結:

  1. Full source codes

  2. The UTXO Set:_Data_Storage#The_UTXO_set_.28chainstate_leveldb.29)

  3. Merkle Tree

  4. Script

  5. “Ultraprune” Bitcoin Core commit

  6. UTXO set statistics

  7. Smart contracts and Bitcoin

  8. Why every Bitcoin user should understand “SPV security”

原文連結:Building Blockchain in Go. Part 6: Transactions 2

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.