用Go语言建立一个简单的区块链part4(1):交易(1)
交易(1)
引言
交易(transaction)是比特幣的核心所在,而區(qū)塊鏈唯一的目的,也正是為了能夠安全可靠地存儲交易。在區(qū)塊鏈中,交易一旦被創(chuàng)建,就沒有任何人能夠再去修改或是刪除它。今天,我們將會開始實現(xiàn)交易。不過,由于交易是很大的話題,我會把它分為兩部分來講:在今天這個部分,我們會實現(xiàn)交易的基本框架。在第二部分,我們會繼續(xù)討論它的一些細節(jié)。
由于比特幣采用的是 UTXO 模型,并非賬戶模型,并不直接存在“余額”這個概念,余額需要通過遍歷整個交易歷史得來。
比特幣交易
點擊 這里 在 blockchain.info 查看下圖中的交易信息。
一筆交易由一些輸入(input)和輸出(output)組合而來:
type Transaction struct {ID []byteVin []TXInputVout []TXOutput }對于每一筆新的交易,它的輸入會引用(reference)之前一筆交易的輸出(這里有個例外,coinbase 交易),引用就是花費的意思。所謂引用之前的一個輸出,也就是將之前的一個輸出包含在另一筆交易的輸入當(dāng)中,就是花費之前的交易輸出。交易的輸出,就是幣實際存儲的地方。下面的圖示闡釋了交易之間的互相關(guān)聯(lián):
注意:
1、有一些輸出并沒有被關(guān)聯(lián)到某個輸入上
2、一筆交易的輸入可以引用之前多筆交易的輸出
3、一個輸入必須引用一個輸出
貫穿本文,我們將會使用像“錢(money)”,“幣(coin)”,“花費(spend)”,“發(fā)送(send)”,“賬戶(account)” 等等這樣的詞。但是在比特幣中,其實并不存在這樣的概念。交易僅僅是通過一個腳本(script)來鎖定(lock)一些值(value),而這些值只可以被鎖定它們的人解鎖(unlock)。
每一筆比特幣交易都會創(chuàng)造輸出,輸出都會被區(qū)塊鏈記錄下來。給某個人發(fā)送比特幣,實際上意味著創(chuàng)造新的 UTXO 并注冊到那個人的地址,可以為他所用。
交易輸出
先從輸出(output)開始:
type TXOutput struct {Value intScriptPubKey string }輸出主要包含兩部分:
1、一定量的比特幣(Value)
2、一個鎖定腳本(ScriptPubKey),要花這筆錢,必須要解鎖該腳本。
實際上,正是輸出里面存儲了“幣”(注意,也就是上面的 Value 字段)。而這里的存儲,指的是用一個數(shù)學(xué)難題對輸出進行鎖定,這個難題被存儲在 ScriptPubKey 里面。在內(nèi)部,比特幣使用了一個叫做 Script 的腳本語言,用它來定義鎖定和解鎖輸出的邏輯。雖然這個語言相當(dāng)?shù)脑?#xff08;這是為了避免潛在的黑客攻擊和濫用而有意為之),并不復(fù)雜,但是我們也并不會在這里討論它的細節(jié)。你可以在這里 找到詳細解釋。
在比特幣中,value 字段存儲的是 satoshi 的數(shù)量,而不是 BTC 的數(shù)量。一個 satoshi 等于一億分之一的
BTC(0.00000001 BTC),這也是比特幣里面最小的貨幣單位(就像是 1 分的硬幣)。
由于還沒有實現(xiàn)地址(address),所以目前我們會避免涉及邏輯相關(guān)的完整腳本。ScriptPubKey 將會存儲一個任意的字符串(用戶定義的錢包地址)。
順便說一下,有了一個這樣的腳本語言,也意味著比特幣其實也可以作為一個智能合約平臺。
關(guān)于輸出,非常重要的一點是:它們是不可再分的(indivisible)。也就是說,你無法僅引用它的其中某一部分。要么不用,如果要用,必須一次性用完。當(dāng)一個新的交易中引用了某個輸出,那么這個輸出必須被全部花費。如果它的值比需要的值大,那么就會產(chǎn)生一個找零,找零會返還給發(fā)送方。這跟現(xiàn)實世界的場景十分類似,當(dāng)你想要支付的時候,如果一個東西值 1 美元,而你給了一個 5 美元的紙幣,那么你會得到一個 4 美元的找零。
發(fā)送幣
現(xiàn)在,我們想要給其他人發(fā)送一些幣。為此,我們需要創(chuàng)建一筆新的交易,將它放到一個塊里,然后挖出這個塊。之前我們只實現(xiàn)了 coinbase 交易(這是一種特殊的交易),現(xiàn)在我們需要一種通用的普通交易:
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {var inputs []TXInputvar outputs []TXOutputacc, validOutputs := bc.FindSpendableOutputs(from, amount)if acc < amount {log.Panic("ERROR: Not enough funds")}// Build a list of inputsfor txid, outs := range validOutputs {txID, err := hex.DecodeString(txid)for _, out := range outs {input := TXInput{txID, out, from}inputs = append(inputs, input)}}// Build a list of outputsoutputs = append(outputs, TXOutput{amount, to})if acc > amount {outputs = append(outputs, TXOutput{acc - amount, from}) // a change}tx := Transaction{nil, inputs, outputs}tx.SetID()return &tx }在創(chuàng)建新的輸出前,我們首先必須找到所有的未花費輸出,并且確保它們有足夠的價值(value),這就是 FindSpendableOutputs 方法要做的事情。隨后,對于每個找到的輸出,會創(chuàng)建一個引用該輸出的輸入。接下來,我們創(chuàng)建兩個輸出:
FindSpendableOutputs 方法基于之前定義的 FindUnspentTransactions 方法:
func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {unspentOutputs := make(map[string][]int)unspentTXs := bc.FindUnspentTransactions(address)accumulated := 0Work:for _, tx := range unspentTXs {txID := hex.EncodeToString(tx.ID)for outIdx, out := range tx.Vout {if out.CanBeUnlockedWith(address) && accumulated < amount {accumulated += out.ValueunspentOutputs[txID] = append(unspentOutputs[txID], outIdx)if accumulated >= amount {break Work}}}}return accumulated, unspentOutputs }這個方法對所有的未花費交易進行迭代,并對它的值進行累加。當(dāng)累加值大于或等于我們想要傳送的值時,它就會停止并返回累加值,同時返回的還有通過交易 ID 進行分組的輸出索引。我們只需取出足夠支付的錢就夠了。
現(xiàn)在,我們可以修改 Blockchain.MineBlock 方法:
func (bc *Blockchain) MineBlock(transactions []*Transaction) {...newBlock := NewBlock(transactions, lastHash)... }最后,讓我們來實現(xiàn) send 方法:
func (cli *CLI) send(from, to string, amount int) {bc := NewBlockchain(from)defer bc.db.Close()tx := NewUTXOTransaction(from, to, amount, bc)bc.MineBlock([]*Transaction{tx})fmt.Println("Success!") }發(fā)送幣意味著創(chuàng)建新的交易,并通過挖出新塊的方式將交易打包到區(qū)塊鏈中。不過,比特幣并不是一連串立刻完成這些事情(雖然我們目前的實現(xiàn)是這么做的)。相反,它會將所有新的交易放到一個內(nèi)存池中(mempool),然后當(dāng)?shù)V工準備挖出一個新塊時,它就從內(nèi)存池中取出所有交易,創(chuàng)建一個候選塊。只有當(dāng)包含這些交易的塊被挖出來,并添加到區(qū)塊鏈以后,里面的交易才開始確認。
讓我們來檢查一下發(fā)送幣是否能工作:
$ blockchain_go send -from Ivan -to Pedro -amount 6 00000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37Success!$ blockchain_go getbalance -address Ivan Balance of 'Ivan': 4$ blockchain_go getbalance -address Pedro Balance of 'Pedro': 6很好!現(xiàn)在,讓我們創(chuàng)建更多的交易,確保從多個輸出中發(fā)送幣也正常工作:
$ blockchain_go send -from Pedro -to Helen -amount 2 00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdfSuccess!$ blockchain_go send -from Ivan -to Helen -amount 2 000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aaSuccess!現(xiàn)在,Helen 的幣被鎖定在了兩個輸出中:一個來自 Pedro,一個來自 Ivan。讓我們把它們發(fā)送給其他人:
$ blockchain_go send -from Helen -to Rachel -amount 3 000000c58136cffa669e767b8f881d16e2ede3974d71df43058baaf8c069f1a0Success!$ blockchain_go getbalance -address Ivan Balance of 'Ivan': 2$ blockchain_go getbalance -address Pedro Balance of 'Pedro': 4$ blockchain_go getbalance -address Helen Balance of 'Helen': 1$ blockchain_go getbalance -address Rachel Balance of 'Rachel': 3看起來沒問題!現(xiàn)在,來測試一些失敗的情況:
$ blockchain_go send -from Pedro -to Ivan -amount 5 panic: ERROR: Not enough funds$ blockchain_go getbalance -address Pedro Balance of 'Pedro': 4$ blockchain_go getbalance -address Ivan Balance of 'Ivan': 2總結(jié)
雖然不容易,但是現(xiàn)在終于實現(xiàn)交易了!不過,我們依然缺少了一些像比特幣那樣的一些關(guān)鍵特性:
交易輸入
這里是輸入:
type TXInput struct {Txid []byteVout intScriptSig string }正如之前所提到的,一個輸入引用了之前交易的一個輸出:Txid 存儲的是之前交易的 ID,Vout 存儲的是該輸出在那筆交易中所有輸出的索引(因為一筆交易可能有多個輸出,需要有信息指明是具體的哪一個)。ScriptSig 是一個腳本,提供了可解鎖輸出結(jié)構(gòu)里面 ScriptPubKey 字段的數(shù)據(jù)。如果 ScriptSig 提供的數(shù)據(jù)是正確的,那么輸出就會被解鎖,然后被解鎖的值就可以被用于產(chǎn)生新的輸出;如果數(shù)據(jù)不正確,輸出就無法被引用在輸入中,或者說,無法使用這個輸出。這種機制,保證了用戶無法花費屬于其他人的幣。
再次強調(diào),由于我們還沒有實現(xiàn)地址,所以目前 ScriptSig 將僅僅存儲一個用戶自定義的任意錢包地址。我們會在下一篇文章中實現(xiàn)公鑰(public key)和簽名(signature)。
來簡要總結(jié)一下。輸出,就是 “幣” 存儲的地方。每個輸出都會帶有一個解鎖腳本,這個腳本定義了解鎖該輸出的邏輯。每筆新的交易,必須至少有一個輸入和輸出。一個輸入引用了之前一筆交易的輸出,并提供了解鎖數(shù)據(jù)(也就是 ScriptSig 字段),該數(shù)據(jù)會被用在輸出的解鎖腳本中解鎖輸出,解鎖完成后即可使用它的值去產(chǎn)生新的輸出。
每一筆輸入都是之前一筆交易的輸出,那么假設(shè)從某一筆交易開始不斷往前追溯,它所涉及的輸入和輸出到底是誰先存在呢?換個說法,這是個雞和蛋誰先誰后的問題,是先有蛋還是先有雞呢?
先有蛋
在比特幣中,是先有蛋,然后才有雞。輸入引用輸出的邏輯,是經(jīng)典的“蛋還是雞”問題:輸入先產(chǎn)生輸出,然后輸出使得輸入成為可能。在比特幣中,最先有輸出,然后才有輸入。換而言之,第一筆交易只有輸出,沒有輸入。
當(dāng)?shù)V工挖出一個新的塊時,它會向新的塊中添加一個 coinbase 交易。coinbase 交易是一種特殊的交易,它不需要引用之前一筆交易的輸出。它“憑空”產(chǎn)生了幣(也就是產(chǎn)生了新幣),這是礦工獲得挖出新塊的獎勵,也可以理解為“發(fā)行新幣”。
在區(qū)塊鏈的最初,也就是第一個塊,叫做創(chuàng)世塊。正是這個創(chuàng)世塊,產(chǎn)生了區(qū)塊鏈最開始的輸出。對于創(chuàng)世塊,不需要引用之前的交易輸出。因為在創(chuàng)世塊之前根本不存在交易,也就沒有不存在交易輸出。
來創(chuàng)建一個 coinbase 交易:
func NewCoinbaseTX(to, data string) *Transaction {if data == "" {data = fmt.Sprintf("Reward to '%s'", to)}txin := TXInput{[]byte{}, -1, data}txout := TXOutput{subsidy, to}tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}tx.SetID()return &tx }coinbase 交易只有一個輸出,沒有輸入。在我們的實現(xiàn)中,它表現(xiàn)為 Txid 為空,Vout 等于 -1。并且,在當(dāng)前實現(xiàn)中,coinbase 交易也沒有在 ScriptSig 中存儲腳本,而只是存儲了一個任意的字符串 data。
在比特幣中,第一筆 coinbase 交易包含了如下信息:“The Times 03/Jan/2009 Chancellor on
brink of second bailout for banks”。可點擊這里查看.
subsidy 是挖出新塊的獎勵金。在比特幣中,實際并沒有存儲這個數(shù)字,而是基于區(qū)塊總數(shù)進行計算而得:區(qū)塊總數(shù)除以 210000 就是 subsidy。挖出創(chuàng)世塊的獎勵是 50 BTC,每挖出 210000 個塊后,獎勵減半。在我們的實現(xiàn)中,這個獎勵值將會是一個常量(至少目前是)。
將交易保存到區(qū)塊鏈
從現(xiàn)在開始,每個塊必須存儲至少一筆交易。如果沒有交易,也就不可能出新的塊。這意味著我們應(yīng)該移除 Block 的 Data 字段,取而代之的是存儲交易:
type Block struct {Timestamp int64Transactions []*TransactionPrevBlockHash []byteHash []byteNonce int }NewBlock 和 NewGenesisBlock 也必須做出相應(yīng)改變:
func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}... }func NewGenesisBlock(coinbase *Transaction) *Block {return NewBlock([]*Transaction{coinbase}, []byte{}) }接下來修改創(chuàng)建區(qū)塊鏈的函數(shù):
func CreateBlockchain(address string) *Blockchain {...err = db.Update(func(tx *bolt.Tx) error {cbtx := NewCoinbaseTX(address, genesisCoinbaseData)genesis := NewGenesisBlock(cbtx)b, err := tx.CreateBucket([]byte(blocksBucket))err = b.Put(genesis.Hash, genesis.Serialize())...})... }現(xiàn)在,這個函數(shù)會接受一個地址作為參數(shù),這個地址將會被用來接收挖出創(chuàng)世塊的獎勵。
工作量證明
工作量證明算法必須要將存儲在區(qū)塊里面的交易考慮進去,從而保證區(qū)塊鏈交易存儲的一致性和可靠性。所以,我們必須修改 ProofOfWork.prepareData 方法:
不像之前使用 pow.block.Data,現(xiàn)在我們使用 pow.block.HashTransactions() :
func (b *Block) HashTransactions() []byte {var txHashes [][]bytevar txHash [32]bytefor _, tx := range b.Transactions {txHashes = append(txHashes, tx.ID)}txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))return txHash[:] }通過哈希提供數(shù)據(jù)的唯一表示,這種做法我們已經(jīng)不是第一次遇到了。我們想要通過僅僅一個哈希,就可以識別一個塊里面的所有交易。為此,先獲得每筆交易的哈希,然后將它們關(guān)聯(lián)起來,最后獲得一個連接后的組合哈希。
比特幣使用了一個更加復(fù)雜的技術(shù):它將一個塊里面包含的所有交易表示為一個 Merkle tree
,然后在工作量證明系統(tǒng)中使用樹的根哈希(root hash)。這個方法能夠讓我們快速檢索一個塊里面是否包含了某筆交易,即只需 root
hash 而無需下載所有交易即可完成判斷。
來檢查一下到目前為止是否正確:
$ blockchain_go createblockchain -address Ivan 00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8aDone!很好!我們已經(jīng)獲得了第一筆挖礦獎勵,但是,我們要如何查看余額呢?
未花費交易輸出
我們需要找到所有的未花費交易輸出(unspent transactions outputs, UTXO)。未花費(unspent) 指的是這個輸出還沒有被包含在任何交易的輸入中,或者說沒有被任何輸入引用。在上面的圖示中,未花費的輸出是:
當(dāng)然了,檢查余額時,我們并不需要知道整個區(qū)塊鏈上所有的 UTXO,只需要關(guān)注那些我們能夠解鎖的那些 UTXO(目前我們還沒有實現(xiàn)密鑰,所以我們將會使用用戶定義的地址來代替)。首先,讓我們定義在輸入和輸出上的鎖定和解鎖方法:
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {return in.ScriptSig == unlockingData }func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {return out.ScriptPubKey == unlockingData }在這里,我們只是將 script 字段與 unlockingData 進行了比較。在后續(xù)文章我們基于私鑰實現(xiàn)了地址以后,會對這部分進行改進。
下一步,找到包含未花費輸出的交易,這一步其實相當(dāng)困難:
func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {var unspentTXs []TransactionspentTXOs := make(map[string][]int)bci := bc.Iterator()for {block := bci.Next()for _, tx := range block.Transactions {txID := hex.EncodeToString(tx.ID)Outputs:for outIdx, out := range tx.Vout {// Was the output spent?if spentTXOs[txID] != nil {for _, spentOut := range spentTXOs[txID] {if spentOut == outIdx {continue Outputs}}}if out.CanBeUnlockedWith(address) {unspentTXs = append(unspentTXs, *tx)}}if tx.IsCoinbase() == false {for _, in := range tx.Vin {if in.CanUnlockOutputWith(address) {inTxID := hex.EncodeToString(in.Txid)spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)}}}}if len(block.PrevBlockHash) == 0 {break}}return unspentTXs }由于交易被存儲在區(qū)塊里,所以我們不得不檢查區(qū)塊鏈里的每一筆交易。從輸出開始:
if out.CanBeUnlockedWith(address) {unspentTXs = append(unspentTXs, tx) }如果一個輸出被一個地址鎖定,并且這個地址恰好是我們要找的地址,那么這個輸出就是我們想要的。不過在獲取它之前,我們需要檢查該輸出是否已經(jīng)被包含在一個交易的輸入中,也就是檢查它是否已經(jīng)被花費了:
if spentTXOs[txID] != nil {for _, spentOut := range spentTXOs[txID] {if spentOut == outIdx {continue Outputs}} }我們跳過那些已經(jīng)被包含在其他輸入中的輸出(這說明這個輸出已經(jīng)被花費,無法再用了)。檢查完輸出以后,我們將給定地址所有能夠解鎖輸出的輸入聚集起來(這并不適用于 coinbase 交易,因為它們不解鎖輸出):
if tx.IsCoinbase() == false {for _, in := range tx.Vin {if in.CanUnlockOutputWith(address) {inTxID := hex.EncodeToString(in.Txid)spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)}} }這個函數(shù)返回了一個交易列表,里面包含了未花費輸出。為了計算余額,我們還需要一個函數(shù)將這些交易作為輸入,然后僅返回一個輸出:
func (bc *Blockchain) FindUTXO(address string) []TXOutput {var UTXOs []TXOutputunspentTransactions := bc.FindUnspentTransactions(address)for _, tx := range unspentTransactions {for _, out := range tx.Vout {if out.CanBeUnlockedWith(address) {UTXOs = append(UTXOs, out)}}}return UTXOs }就是這么多了!現(xiàn)在我們來實現(xiàn) getbalance 命令:
func (cli *CLI) getBalance(address string) {bc := NewBlockchain(address)defer bc.db.Close()balance := 0UTXOs := bc.FindUTXO(address)for _, out := range UTXOs {balance += out.Value}fmt.Printf("Balance of '%s': %d\n", address, balance) }賬戶余額就是由賬戶地址鎖定的所有未花費交易輸出的總和。
在挖出創(chuàng)世塊以后,來檢查一下我們的余額:
$ blockchain_go getbalance -address Ivan Balance of 'Ivan': 10這就是我們的第一筆錢!
我最新的代碼已經(jīng)上傳到百度云盤上了,需要的可自行下載,永久有效!
鏈接:https://pan.baidu.com/s/1n-SZaLhRKnxVHs1ONJs-Ug 密碼:2ihj
總結(jié)
以上是生活随笔為你收集整理的用Go语言建立一个简单的区块链part4(1):交易(1)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用Go语言建立一个简单的区块链part3
- 下一篇: 用Go语言建立一个简单的区块链part5