2.7 交易確認
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集合就會被更新。
檢測一下是否工作:
blockchaingocreateblockchain−address1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ100000086a725e18ed7e9e06f1051651a4fc46a315a9d298e59e57aeacbe0bf73Done!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),然後用提供的及檢測這個簽名是否正確。
擁有指令碼語言的使得比特幣可能成為智慧合約平臺:這個語言除了能支援每次交易都使用單一的金鑰轉移比特幣,其它的支付場景也成為可能。
總結
好了,我們實現了大多數基於區塊鏈加密貨幣的差關鍵特性。區塊鏈、地址、挖礦、交易。但是,還有一個賦予這些特性生命的機制,創造了比特幣的全域性系統,一致性。下一章我們開始實現區塊鏈的一部分–“去中心化”。敬請期待!
-
學院Go語言視訊主頁
https://edu.csdn.net/lecturer/1928 -
掃碼獲取海量視訊及原始碼 QQ群:721929980
相關文章
- 什麼是“即時交易”與BCH的零確認安全問題解析
- 重大事件確認、進度確認和時間事件三種報工確認方式事件
- RabitMQ 釋出確認MQ
- ECMAScript 2021 正式確認
- 資料資產確權認責,確什麼權,認什麼責
- 軟體確認測試
- RabbitMq之訊息確認MQ
- 2.7
- RabbitMQ 訊息確認機制MQ
- 2.7(2)
- layui 樹形結構刪除沒有確認,原始碼加入confirm確認提示框UI原始碼
- 2.7萬隻!當前及未來仍將是量化交易的黃金期
- 軟體產品確認測試
- Mysql利用explain確認是否使用索引MySqlAI索引
- SAP WM 能否使用LT12去確認一個需要2-step法確認的TO單?
- 軟體確認測試的內容和流程有哪些?確認測試報告需要多少錢?測試報告
- 禁止ssh連線時的確認提示
- 確認末尾字元演算法挑戰字元演算法
- 如何在 Linux 下確認 NTP 是否同步?Linux
- 確認過眼神,你就是我的Promise~~Promise
- 2.7(學號:3025)
- 2.7 Lab: Circular Shift
- TypeScript 2.7 記錄TypeScript
- 軟體測試的驗證和確認
- 如何確認DFMEA的傳遞是有效的?
- 正確地使用加密與認證技術加密
- 印尼央行確認遭受勒索軟體攻擊
- 身份認證之如何確定你就是“你”?
- jQuery Validate驗證確認密碼是否相同jQuery密碼
- Fuchsia最新訊息,確認支援Android應用Android
- JAVA訊息確認機制之ACK模式Java模式
- RabbitMQ中釋出者透過確認機制確保訊息釋出MQ
- 軟體產品確認測試包括哪些方面? 軟體確認測試報告2022費用價目表測試報告
- 你瞭解過軟體確認測試嗎?可進行確認測試的軟體測評中心推薦
- 訂單自動確認或取消設計方案
- 軟體測試需求怎麼確認和梳理?
- 如何正確認識代理伺服器的使用伺服器
- 如何確認自己的產品是否適合做SEO?