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
是什麼:
'c' + 32-byte transaction hash -> unspent transaction output record for that transaction
'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集。目前,以下方法用於查詢事務:
Blockchain.FindUnspentTransactions
- 查詢具有未使用輸出的事務的主函式。這是所有塊的迭代發生的功能。Blockchain.FindSpendableOutputs
- 建立新事務時使用此功能。如果找到足夠數量的輸出保持所需數量,會使用Blockchain.FindUnspentTransactions
.Blockchain.FindUTXO
- 查詢公鑰雜湊的未使用輸出,用於獲得結餘。會使用Blockchain.FindUnspentTransactions
.Blockchain.FindTransaction
- 通過其ID在區塊鏈中查詢交易。它迭代所有塊直到找到它。
如您所見,所有方法都迭代資料庫中的塊。但是我們現在無法改進所有這些,因為UTXO集不儲存所有事務,而只儲存那些具有未使用輸出的事務。因此,它不能用於Blockchain.FindTransaction
.
所以,我們需要以下方法:
Blockchain.FindUTXO
- 通過迭代塊找到所有未使用的輸出。UTXOSet.Reindex
— 使用FindUTXO
找到未使用的輸出,並將它們儲存在資料庫中。這是快取發生的地方。UTXOSet.FindSpendableOutputs
– 類似Blockchain.FindSpendableOutputs
,但使用UTXO集。UTXOSet.FindUTXO
– 類似Blockchain.FindUTXO
,但使用UTXO集。Blockchain.FindTransaction
和原來保持一致.
因此,兩個最常用的函式將從現在開始使用快取!我們開始編碼了。
type UTXOSet struct {
Blockchain *Blockchain
}
複製程式碼
我們將使用單個資料庫,但我們會將UTXO集儲存在不同的儲存桶中。從而,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集。首先,如果存在,則刪除儲存桶,然後從區塊鏈中獲取所有未使用的輸出,最後將輸出儲存到儲存桶。
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次:
- 一次來自於挖掘創世塊。
- 一次來自於開採塊
0000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21b
. - 一次來自於開採塊
000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433
.
Merkle Tree
我想在這篇文章中討論另外一種優化機制。
如上所述,完整的比特幣資料庫(即區塊鏈)需要超過140 Gb的磁碟空間。由於比特幣的分散性,網路中的每個節點必須是獨立且自給自足的,即每個節點必須儲存區塊鏈的完整副本。隨著許多人開始使用比特幣,這條規則變得更難以遵循:每個人都不可能執行完整的節點。此外,由於節點是網路的成熟參與者,因此他們有責任:他們必須驗證交易和阻止。此外,與其他節點互動並下載新塊需要一定的網際網路流量。
In the original Bitcoin paper由Satoshi Nakamoto釋出,有一個解決這個問題的方法:簡化付款驗證(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
}
複製程式碼
建立新樹時,首先要確保的是有一些偶數葉子。之後,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
還有一件事我想更詳細地討論。
你還記得,在比特幣中有
5 2 OP_ADD 7 OP_EQUAL
複製程式碼
5
, 2
, 以及 7
是 data. OP_ADD
和 OP_EQUAL
是 operators.
讓我們將上述指令碼的執行分解為以下步驟:
- Stack:空。Script:
5 2 OP_ADD 7 OP_EQUAL
. - Stack:
5
. Script:2 OP_ADD 7 OP_EQUAL
. - Stack:
5 2
. Script:OP_ADD 7 OP_EQUAL
. - Stack:
7
. Script:7 OP_EQUAL
. - Stack:
7 7
. Script:OP_EQUAL
. - Stack:
true
. Script: empty.
OP_ADD
從堆疊中獲取兩個元素,彙總它們,並將總和推入堆疊。OP_EQUAL
從堆疊中取出兩個元素並對它們進行比較:如果它們相等則會推動它們true
到堆疊;否則它會推動false
。指令碼執行的結果是頂部堆疊元素的值:在我們的例子中,它是true
,這意味著指令碼成功完成。
現在讓我們看看比特幣中用於執行付款的指令碼:
<signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
複製程式碼
呼叫此指令碼
該指令碼實際上儲存在兩個部分:
- 第一塊,
<signature> <pubKey>
,儲存在輸入中ScriptSig
欄位. - 第二塊,
OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
儲存在輸出中ScriptPubKey
.
因此,它的輸出定義瞭解鎖邏輯,它的輸入提供資料來解鎖輸出。讓我們執行指令碼:
- Stack: 空 Script:
<signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
- Stack:
<signature>
Script:<pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
- Stack:
<signature> <pubKey>
Script:OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
- Stack:
<signature> <pubKey> <pubKey>
Script:OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
- Stack:
<signature> <pubKey> <pubKeyHash>
Script:<pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
- Stack:
<signature> <pubKey> <pubKeyHash> <pubKeyHash>
Script:OP_EQUALVERIFY OP_CHECKSIG
- Stack:
<signature> <pubKey>
Script:OP_CHECKSIG
- Stack:
true
orfalse
. Script: empty.
OP_DUP
複製頂部堆疊元素。OP_HASH160
採用頂部堆疊元素並將其雜湊RIPEMD160
;結果被推回堆疊。OP_EQUALVERIFY
比較兩個頂部堆疊元素,如果它們不相等,則中斷指令碼。OP_CHECKSIG
通過雜湊事務並使用來驗證事務的簽名<signature>
和<pubKey>
。後一個運算子非常複雜:它會對事務進行修剪,對其進行雜湊處理(因為它是已簽名的事務的雜湊),並使用提供的方式檢查簽名是否正確<signature>
和 <pubKey>
.
擁有這樣的指令碼語言使得比特幣也成為智慧合約平臺:除了轉移到單個金鑰之外,該語言還可以實現其他支付方案。例如,
Conclusion
就是這樣!我們已經實現了基於區塊鏈的加密貨幣的幾乎所有關鍵功能。我們有區塊鏈,地址,挖掘和交易。但是還有一件事給所有這些機制賦予生命,並使比特幣成為一個全球系統:共識。在下一篇文章中,我們將開始實現區塊鏈的“分散”部分。敬請關注!