2.7 交易確認

尹成發表於2018-11-08

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集合就會被更新。 
檢測一下是否工作: 
blockchaingocreateblockchainaddress1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ100000086a725e18ed7e9e06f1051651a4fc46a315a9d298e59e57aeacbe0bf73Done!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計算它們。這當然也是獲取唯一的區塊中的交易描述的好方式,但是並沒有默克爾樹的優點。 
看看默克爾樹: 

默克爾樹為了每一個區塊而建立,開始於葉(樹的底部)節點,葉子就是一個交易的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),然後用提供的及檢測這個簽名是否正確。 
擁有指令碼語言的使得比特幣可能成為智慧合約平臺:這個語言除了能支援每次交易都使用單一的金鑰轉移比特幣,其它的支付場景也成為可能。

總結

好了,我們實現了大多數基於區塊鏈加密貨幣的差關鍵特性。區塊鏈、地址、挖礦、交易。但是,還有一個賦予這些特性生命的機制,創造了比特幣的全域性系統,一致性。下一章我們開始實現區塊鏈的一部分–“去中心化”。敬請期待!

 

 

相關文章