在Go中構建區塊鏈 第6部分:交易2

銀河1號發表於2019-02-22

Introduction

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

Reward

我們在之前的文章中跳過的一件小事就是採礦獎勵。我們已經擁有了實現它的一切。

獎勵只是一個coinbase transaction。當挖掘節點開始挖掘新塊時,它從佇列中獲取事務並將coinbase transaction預先新增到它們。 coinbase transaction的唯一輸出包含礦工的公鑰雜湊。

實現獎勵就像更新獎勵一樣簡單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!")
}
複製程式碼

在我們的實現中,建立事務的人挖掘新塊,從而獲得獎勵。

The UTXO Set

在第三章中我們研究了比特幣在資料庫中儲存塊的方式。塊儲存在blocks資料庫,事務輸出儲存在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

自那篇文章以來,我們已經實現了交易,但我們還沒有使用過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集儲存在不同的儲存桶中。從而,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集。首先,如果存在,則刪除儲存桶,然後從區塊鏈中獲取所有未使用的輸出,最後將輸出儲存到儲存桶。

Blockchain.FindUTXO幾乎與Blockchain.FindUnspentTransactions完全相同,但現在它返回一個對映TransactionID → TransactionOutputs .

現在,UTXO集可用於傳送coins:

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 方法對應的略微修改過的版本.

設定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
複製程式碼

Nice! 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1地址收到獎勵3次:

  1. 一次來自於挖掘創世塊。
  2. 一次來自於開採塊0000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21b.
  3. 一次來自於開採塊000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433.

Merkle Tree

我想在這篇文章中討論另外一種優化機制。

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

In the original Bitcoin paper由Satoshi Nakamoto釋出,有一個解決這個問題的方法:簡化付款驗證(SPV)。 SPV是一個輕便的比特幣節點,不會下載整個區塊鏈和不驗證塊和事務。相反,它在塊中查詢事務(以驗證付款)並連結到完整節點以檢索必要的資料。此機制允許多個輕型錢包節點僅執行一個完整節點。

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

比特幣使用Merkle樹來獲取事務雜湊值,然後將其儲存在塊頭中並由工作量證明系統考慮。到目前為止,我們只是在塊中連線每個事務的雜湊並應用SHA-256給他們。這也是獲得塊事務的唯一表示的好方法,但它沒有Merkle樹的好處。

讓我們看一下Merkle樹:

在Go中構建區塊鏈 第6部分:交易2


為每個塊構建一個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
}
複製程式碼

建立新樹時,首先要確保的是有一些偶數葉子。之後,data(這是一系列序列化事務)被轉換為樹葉,並從這些葉子生長樹。

現在, 讓我們修改 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樹。樹的根將作為塊事務的唯一識別符號。

P2PKH

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

你還記得,在比特幣中有

Script
程式語言,用於鎖定事務輸出;和交易輸入提供解鎖輸出的資料。語言很簡單,這種語言的程式碼只是一系列資料和運算子。考慮這個例子:

5 2 OP_ADD 7 OP_EQUAL
複製程式碼

5, 2, 以及 7 是 data. OP_ADDOP_EQUAL 是 operators.

Script
程式碼從左到右執行:每個資料都放入堆疊,下一個操作符應用於頂層堆疊元素。
Script
堆疊只是一個簡單的FILO(第一輸入最後輸出)記憶體儲存:堆疊中的第一個元素是最後一個元素,每個元素都放在前一個元素上。

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

  1. Stack:空。Script:5 2 OP_ADD 7 OP_EQUAL.
  2. Stack: 5. Script: 2 OP_ADD 7 OP_EQUAL.
  3. Stack: 5 2. Script: OP_ADD 7 OP_EQUAL.
  4. Stack: 7. Script: 7 OP_EQUAL.
  5. Stack: 7 7. Script: OP_EQUAL.
  6. Stack: true. Script: empty.

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

現在讓我們看看比特幣中用於執行付款的指令碼:

<signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
複製程式碼

呼叫此指令碼

支付公鑰雜湊
(P2PKH),這是比特幣中最常用的指令碼。它實際上支付公鑰雜湊,即用某個公鑰鎖定硬幣。這是比特幣支付的核心:沒有賬戶,沒有資金轉移;只有一個指令碼可以檢查提供的簽名和公鑰是否正確。

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

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

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

  1. Stack: 空 Script: <signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
  2. Stack: <signature>Script: <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
  3. Stack: <signature> <pubKey>Script: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
  4. Stack: <signature> <pubKey> <pubKey>Script: OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
  5. Stack: <signature> <pubKey> <pubKeyHash>Script: <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
  6. Stack: <signature> <pubKey> <pubKeyHash> <pubKeyHash>Script: OP_EQUALVERIFY OP_CHECKSIG
  7. Stack: <signature> <pubKey>Script: OP_CHECKSIG
  8. Stack: true or false. Script: empty.

OP_DUP複製頂部堆疊元素。OP_HASH160採用頂部堆疊元素並將其雜湊RIPEMD160;結果被推回堆疊。OP_EQUALVERIFY比較兩個頂部堆疊元素,如果它們不相等,則中斷指令碼。OP_CHECKSIG通過雜湊事務並使用來驗證事務的簽名<signature><pubKey>。後一個運算子非常複雜:它會對事務進行修剪,對其進行雜湊處理(因為它是已簽名的事務的雜湊),並使用提供的方式檢查簽名是否正確<signature><pubKey>.

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

Conclusion

就是這樣!我們已經實現了基於區塊鏈的加密貨幣的幾乎所有關鍵功能。我們有區塊鏈,地址,挖掘和交易。但是還有一件事給所有這些機制賦予生命,並使比特幣成為一個全球系統:共識。在下一篇文章中,我們將開始實現區塊鏈的“分散”部分。敬請關注!

原文連結:jeiwan.cc/posts/build…

更多文章歡迎訪問: http://www.apexyun.com

聯絡郵箱:public@space-explore.com

(未經同意,請勿轉載)


相關文章