2.5 交易

尹成發表於2018-11-08

There is no spoon

如果你曾經做過關於交易的web應用,那麼會應該會建立類似的兩張表,account,transaction。account表用於存放使用者資訊和餘額,而金額交易記錄會存在transaction表。而在比特幣中,支付是完全不同的方式: 
1. 沒有賬戶 
2. 沒有餘額 
3. 沒有地址 
4. 沒有貨幣 
5. 沒有支付方和收款方 
因為區塊鏈是公用和開放的資料庫,所以並不會存放敏感的有關錢包的資料。貨幣並不在賬戶中,交易也不是把錢從一個地址轉到另一個地址。也沒有欄位或屬性來儲存賬戶的餘額。只有交易本身,那又有什麼在交易裡呢?所以,這將會摧毀生活給我們樹下的交易固有概念,也就是說 There is no spoon。

比特幣交易

比特幣的交易結構中,input與output是在一起的(input與output進一步闡述):

type Transaction struct {
        ID   []byte
        Vin  []TXInput
        Vout []TXOutput
}

新交易input會關聯到前一筆output(有例外,稍後補充)。output是比特幣真實儲存的地方。下面的這張圖展示了交易的關係:  
注意: 
1. 有output是沒有與input關聯的 
2. 在一筆交易中,input可以與不同的交易中的output相關聯。 
3. 而input一定是會關聯一筆output的 
本章全篇,我們使用了“錢”、“幣”、“消費”、“傳送”、“賬戶”等等,而比特幣裡是沒有這些概念的。交易中(比特幣機制)會使用指令碼(script)鎖住相關的值,然後也只有加鎖的才能解開這鎖。

交易output

從output的結構開始:

type TXOutput struct {
        Value        int
        ScriptPubKey string
}

事實上,output儲存了“幣”(上面的Value)。儲存的意思是使用一串無法破解的方式(謎,puzzle)鎖住這些幣,這個puzzle就儲存在ScriptPubKey中。在內部,Bitcoin使用了一種叫做Script的指令碼語言,用這個Script來定義output鎖和解鎖的邏輯。這個語言是相當原始的,故意這樣做是為了避免被攻擊和濫用,但是這裡不進行深一步的討論。可以在這裡找到更詳細的解釋。 
In Bitcoin, the value field stores the number of satoshis, not the number of BTC. A satoshi is a hundred millionth of a bitcoin (0.00000001 BTC), thus this is the smallest unit of currency in Bitcoin (like a cent). 
在比特幣中,value儲存了satoshis的數量,並不是BTC的值。一個satoshis就是一億分之一個BTC,所以這是比特幣當前最小的單位(差不多是相當於分) 
因為我們現在還沒有實現地址(address),所以我們會避免整個和指令碼有關的邏輯。ScriptPubKey也會隨便插入一個字串(使用者定義的錢包地址)。 
順便說一句,使用指令碼語言意味著比特幣可以也作為智慧合約平臺。 
還有一個重要的事情是output是不能分隔的,所以你不能只引用它的一部分。如果一個output在一個交易中被關聯,那麼它就會全部消費掉。而如果該output的值是大於交易所需的,那麼會有一筆“change”產生並返回傳送者(消費者)。這和現實生活中的交易是差不多的,比如花5美元的紙幣去買值1美元的東西,那你會收到4美元的找零。

交易input

input的結構

type TXInput struct {
        Txid      []byte
        Vout      int
        ScriptSig string
}

先前提到,input引用了前面的output。Txid儲存了交易的id,而Vout則儲存該交易的中一個output索引。ScriptSig就是負責提供在與output的ScriptPubKey中對比的資料,如果資料正確,那麼這個被引用的output就可以被解鎖,而它裡面的值可以產生新的output。如果不正確,這個output就不能被這個input引用。這個機制就避免了有人會去消費別人的比特幣。 
再強調一點,因為我們還沒有地址(address),ScriptSig僅只是儲存了一個任意的使用者定義的錢包地址。我們將在下一章中實現公鑰和簽名檢測。 
總結一下,output就是“幣”存的位置。每一個output都來自一個解鎖了的script,這人script決定了解鎖這個output的邏輯。每一個新的交易都必須有一個input和output。而input關聯的前面的交易中的output,並且提供資料(ScriptSig欄位)去解鎖output和它裡面的幣而後用這些幣去建立新的output。 
那接下來,是先有input還是output呢?

先有蛋再有雞

在比特幣的世界裡,是先雞再有蛋。輸入關聯輸出的邏輯( inputs-referencing-outputs logic )就是經典的“先有雞還是先有蛋”問題的情況:由input生成output,然後output使得input的過程行得通。而在比特幣中,output比input出現得早,input是雞,output是蛋。 
當礦機開始去挖一個區塊時,它增加了coinbase transaction的交易。而“coinbase transaction”是一種特殊型別的交易,它不需要任何output。它會無中生有output(比如:“幣”)。從而蛋不是雞生的。這是給礦工挖出新區塊的獎勵。 
前面的章節裡提到的創世區塊就是整個區塊鏈的起始點。就是這個創世區塊在區塊鏈中生成了第一個output。因為沒有更早的交易,所以沒有更早的output。 
建立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交易只能有一個input。在我們的實現裡,Txid是空的,而Vout是-1。另外,coinbase也不需要儲存ScriptSig。相反,有任意的資料儲存在這裡。 
In Bitcoin, the very first coinbase transaction contains the following message: “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”. You can see it yourself. 
比特幣中, 最新的coinbase交易訊息裡有這麼一段:“《泰晤士報》,2009年1月3日,財政大臣正站在第二輪救助銀行業的邊緣”。 
subsidy補貼就是獎勵的數量。在比特幣中,這個數字並沒有儲存在任何地方,也僅是通過區塊的總數計算出來:區塊的總數除以210000。挖出創世區塊價值50個BTC,每210000塊區塊被挖出,比特幣單位產量就會減半(210001塊到420000塊時,只值25BTC了)。在我們的實現中,我們將會用一個常量來儲存這個獎勵(目前來說是如此?)。

儲存交易

現在,每個區塊都必須至少儲存一筆交易,並且再也不可能不通過交易而挖出新區塊。這意味著我們應該刪除Block類中的Data欄位,換成Transactions。

type Block struct {
        Timestamp     int64
        Transactions  []*Transaction
        PrevBlockHash []byte
        Hash          []byte
        Nonce         int
}

NewBlock及NewGenesisBlock也要相應作更改。

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{})
}

下一個改動的是建立新區塊鏈:

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())
                ...
        })
        ...
}

CreateBlockchain函式使用將存放挖出創世區塊的地址address

 

工作量證明

“Proof-of-Work”演算法必須考慮到儲存在區塊中的交易,在區塊鏈中,對於儲存交易的地方,要保證一致性而可靠性。所以要修改一下prepareData方法。

現在不能使用pow.block.Data了,得使用pow.block.HashTransactions():

func (b *Block) HashTransactions() []byte {
        var txHashes [][]byte
        var txHash [32]byte
for _, tx := range b.Transactions {
                txHashes = append(txHashes, tx.ID)
        }
        txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))
return txHash[:]
}

我們再一次使用hash作為提供資料唯一表現的機制。必須保證所有交易在區塊中都有確定唯一的hash值。為了實現這一點,我們計算每一個交易的hash,把它們連線起來,再計算合起來的hash。 

Bitcoin uses a more elaborate technique: it represents all transactions containing in a block as a Merkle treeand uses the root hash of the tree in the Proof-of-Work system. This approach allows to quickly check if a block contains certain transaction, having only just the root hash and without downloading all the transactions. 
比特幣使用了更加精細的技術:把所有交易都維護在一棵默克爾樹中,並“Proof-of-Work”工作量證明中使用樹根的hash值。這樣做可以快速檢測是否區塊包含有指定的交易,僅需要樹的根節點而不需要下載整棵樹。

Output結餘

現在需要找出交易中output的結餘(UTXO, unspent transaction outputs)。Unspent(結餘)意思是這些output並沒有關聯到任何input,在上面的那張圖中,有: 
1. tx0, output 1; 
2. tx1, output 0; 
3. tx3, output 0; 
4. tx4, output 0. 
當然,我們需要檢測餘額,並不需要檢測上面的全部,只需要檢測那些我們的私鑰能解鎖的output(我們目前沒有實現金鑰,通過使用使用者定義的地址作為替代)。現在定義在input和output上增加加鎖和解鎖方法:

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
        return in.ScriptSig == unlockingData
}
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
        return out.ScriptPubKey == unlockingData
}

我們簡單地通過比較script的欄位來判斷是否能解鎖。我們會在後面的章節中,等實現了基於私鑰建立地址,再實現真正的加解鎖。 
下一步,找到有結餘output的交易,這個比較麻煩:

func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
  var unspentTXs []Transaction
  spentTXOs := 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
}

因為交易是被儲存在區塊中的,我們必須去檢測區塊鏈中的每一區塊。 
我們從output開始:

if out.CanBeUnlockedWith(address) {
        unspentTXs = append(unspentTXs, tx)
}

如果鎖住output的地址和我們傳進來的一樣,那麼我們要找的就是該output。但是在這之前,得檢測output是否已經被input引用:

if spentTXOs[txID] != nil {
        for _, spentOut := range spentTXOs[txID] {
                if spentOut == outIdx {
                        continue Outputs
                }
        }
}

跳過已經被input引用的,因為這些值已經被移動到其它output中,導致我們不能再去計算它。在檢測output後,我們收集了所有能解鎖對應地址output的input(這裡不適用於coinbase交易,因為它不需要解鎖output):

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)
        }
    }
}

為了計算餘額,還需要能把FindUnspentTransactions返回的transaction中的output剝出來:

func (bc *Blockchain) FindUTXO(address string) []TXOutput {
       var UTXOs []TXOutput
       unspentTransactions := bc.FindUnspentTransactions(address)
for _, tx := range unspentTransactions {
               for _, out := range tx.Vout {
                       if out.CanBeUnlockedWith(address) {
                               UTXOs = append(UTXOs, out)
                       }
               }
       }
return UTXOs
}

再給CIL增加getBalance指令:

func (cli *CLI) getBalance(address string) {
        bc := NewBlockchain(address)
        defer bc.db.Close()
balance := 0
        UTXOs := bc.FindUTXO(address)
for _, out := range UTXOs {
                balance += out.Value
        }
fmt.Printf("Balance of '%s': %d\n", address, balance)
}

賬戶餘額就是有結餘的交易中被賬戶地址鎖住的output的value總和。 
檢測一下挖出創世區塊時的餘額: 
$ blockchain_go getbalance -address Ivan 
創世區塊給我們帶來了10個BTC的收益。

傳送幣

現在,我們要把幣送給其它人。為了實現這個,需要建立一筆交易,把它設到區塊中,然後挖出這個區塊。到目前為止,我們的程式碼也只是實現了coinbase交易,現在需要一個普通的交易。

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
        var inputs []TXInput
        var outputs []TXOutput
acc, validOutputs := bc.FindSpendableOutputs(from, amount)
if acc < amount {
                log.Panic("ERROR: Not enough funds")
        }
// Build a list of inputs
        for 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 outputs
        outputs = 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
}

在建立新的output前,首先得找到所有有結餘的output,並且要有足夠的值來消費。FindSpendableOutputs方法負責做這事。然後,對於找到的能用的每一個ouput,都會有一個input關聯它們。下一步,我們建立兩個output: 
1. 一個被接收者的地址鎖住。這個output是真正的被傳送到其它地址的幣。 
2. 一個被髮送者的地址鎖住。這個是找零(change)。僅是在進行結餘的output的總額大於需要傳送給接收者所需值的交易時才會被建立。還有,output是不可以分隔的; 
FindSpendableOutputs基於前面定義的FindUnspentTransactions方法:

func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {
        unspentOutputs := make(map[string][]int)
        unspentTXs := bc.FindUnspentTransactions(address)
        accumulated := 0
Work:
        for _, tx := range unspentTXs {
                txID := hex.EncodeToString(tx.ID)
for outIdx, out := range tx.Vout {
                        if out.CanBeUnlockedWith(address) && accumulated < amount {
                                accumulated += out.Value
                                unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)
if accumulated >= amount {
                                        break Work
                                }
                        }
                }
        }
return accumulated, unspentOutputs
}

該方法遍歷所有有結餘的交易,彙總它們的值,當彙總的值等於或大於需要傳送到其它地址的值時,就會停止查詢,立即返回已經彙總到的值和以交易id分組的output索引陣列。不需要找到比本次傳送額更多的output。 
現在修改Blockchain.MineBlock方法:

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
        ...
        newBlock := NewBlock(transactions, lastHash)
        ...
}

最後,實現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!")
}

傳送幣到其它地址,意味著會建立新的交易,然後會通過挖出新的區塊,把交易放到該區塊中,再把該區塊放到區塊鏈的方式讓交易得以在區塊鏈中。但是區塊鏈並不會立即做到這一步,相反,它把所有的交易放到儲存池中,當礦機準備好挖區塊時,它就把儲存池中的所有交易拿出來並建立候選的區塊。交易只有在包含了該交易的區塊被挖出且附加到區塊鏈中時才會被確認。 
現在看看傳送幣的工作是否正常:

$ blockchain_go send -from Ivan -to Pedro -amount 6

00000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37 
Success!

$ blockchain_go getbalance -address Ivan

Balance of ‘Ivan’: 4

$ blockchain_go getbalance -address Pedro

Balance of ‘Pedro’: 6 
再建立幾筆交易,然後確認多個output在花費過程中是否工作正常:

$ blockchain_go send -from Pedro -to Helen -amount 2

00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdf 
Success!

$ blockchain_go send -from Ivan -to Helen -amount 2

000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aa 
Success! 
Helen的幣被兩個output鎖(只有自己的地址才能解鎖)在了兩個output中,一個是Pedro,另一個是Ivan。現在再傳給其他人:

$ blockchain_go send -from Helen -to Rachel -amount 3

000000c58136cffa669e767b8f881d16e2ede3974d71df43058baaf8c069f1a0 
Success!

$ 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 
現在Pedro只有4個幣了,再嘗試把向Ivan傳送5個:

$ 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 
正常~

本章總結

呼!不是很容易,至少現在有交易了。儘管關鍵的特性像比特幣那樣的加密貨幣還沒有實現: 
1. 地址。我們沒有實現真正的地址,基於私鑰的地址。 
2. 獎勵。現在挖出區塊是沒有甜頭的。 
3. UTXO 集合。獲取餘額需要查詢整個區塊,如果有很多的區塊鏈時需要花費非常長的時間。並且,要驗證後續的交易,也會花費大量的時間。UTXO集合就是為了解決這個問題,讓對整個交易的操作更快些。 
4. 儲存池(Mempool)。這裡儲存那些等著被打包到區塊中的交易。在我們的當前的實現裡,一個區塊只有一個交易,這很沒有效率。

 

相關文章