本章源代碼地址:[https://github.com/daleboy/blockchain4]
# 關于區塊鏈的交易
交易是一件復雜的事情,我們在本章中實現交易的通用部分,后面將就細節進行實現。
交易(transaction)是比特幣的核心所在,而區塊鏈的唯一目的,也正是為了能夠安全可靠地存儲交易。在區塊鏈中,交易一旦被創建,就沒有任何人能夠再去修改或是刪除它。
與現實不同,在比特幣中,支付是另外一種完全不同的方式:
1. 沒有賬戶(account)
2. 沒有余額(balance)
3. 沒有住址(address)
4. 沒有貨幣(coin)
5. 沒有發送人和接收人
# 比特幣交易
一筆交易由一些輸入(input)和輸出(output)組合而來:

~~~
type Transaction struct {//交易的結構
ID []byte
Vin []TXInput
Vout []TXOutput
}
~~~
對于每一筆新的交易,它的輸入會引用(reference)之前一筆交易的輸出(這里有個例外,也就是我們待會兒要談到的 coinbase 交易)。所謂引用之前的一個輸出,也就是將之前的一個輸出包含在另一筆交易的輸入當中。交易的輸出,也就是幣實際存儲的地方。
注意:
1. 有一些輸出并沒有被關聯到某個輸入上
2. 一筆交易的輸入可以引用之前多筆交易的輸出
3. 一個輸入必須引用一個輸出
貫穿本文,我們將會使用像“錢(money)”,“幣(coin)”,“花費(spend)”,“發送(send)”,“賬戶(account)” 等等這樣的詞。但是在比特幣中,實際并不存在這樣的概念。交易僅僅是通過一個腳本(script)來鎖定(lock)一些價值(value),而這些價值只可以被鎖定它們的人解鎖(unlock)。
# 交易輸出
讓我們先從輸出(output)開始:
~~~
type TXOutput struct {//交易輸出的結構
Value int//存儲的幣
ScriptPubKey string//解鎖腳本,這個腳本定義了解鎖該輸出的邏輯。
}
~~~
實際上,正是輸出里面存儲了“幣”(注意,也就是上面的`Value`字段)。而這里的存儲,指的是用一個數學難題對輸出進行鎖定,這個難題被存儲在`ScriptPubKey`里面。在內部,比特幣使用了一個叫做*Script*的腳本語言,用它來定義鎖定和解鎖輸出的邏輯。雖然這個語言相當的原始(這是為了避免潛在的黑客攻擊和濫用而有意為之),并不復雜,但是我們并不會在這里討論它的細節。你可以在[這里](https://link.jianshu.com/?t=https://en.bitcoin.it/wiki/Script)找到詳細解釋。
~~~
在比特幣中,`value`字段存儲的是*satoshi*的數量,而不是>有 BTC 的數量。一個*satoshi*等于一百萬分之一的 >BTC(0.00000001 BTC),這也是比特幣里面最小的貨幣單位>(就像是 1 分的硬幣)。
~~~
由于還沒有實現地址(address),所以目前我們會避免涉及邏輯相關的完整腳本。`ScriptPubKey`將會存儲一個任意的字符串(用戶定義的錢包地址)。
~~~
順便說一下,有了一個這樣的腳本語言,也意味著比特幣其實也可以作為一個智能合約平臺。
~~~
關于輸出,非常重要的一點是:它們是**不可再分的(invisible)**,這也就是說,你無法僅引用它的其中某一部分。要么不用,如果要用,必須一次性用完。當一個新的交易中引用了某個輸出,那么這個輸出必須被全部花費。如果它的值比需要的值大,那么就會產生一個找零,找零會返還給發送方。
# 交易輸入
這里是輸入:
~~~
type TXInput struct {//交易輸入的結構
Txid []byte//這筆交易的 ID
Vout int//該輸出在這筆交易中所有輸出的索引(因為一筆交易可能有多個輸出,需要有信息指明是具體的哪一個)。
ScriptSig string
}
~~~
`ScriptSig`是一個腳本,提供了可作用于一個輸出的`ScriptPubKey`的數據。如果`ScriptSig`提供的數據是正確的,那么輸出就會被解鎖,然后被解鎖的值就可以被用于產生新的輸出;如果數據不正確,輸出就無法被引用在輸入中,或者說,也就是無法使用這個輸出。這種機制,保證了用戶無法花費屬于其他人的幣。
再次強調,由于我們還沒有實現地址,所以`ScriptSig`將僅僅存儲一個任意用戶定義的錢包地址。我們會在下一篇文章中實現公鑰(public key)和簽名(signature)。
在比特幣中,每一筆輸入都是之前一筆交易的輸出,那么從一筆交易開始不斷往前追溯,它涉及的輸入和輸出到底是誰先存在呢?答案是:最先有輸出,然后才有輸入。換而言之,第一筆交易只有輸出,沒有輸入。
當礦工挖出一個新的塊時,它會向新的塊中添加一個**coinbase**交易。coinbase 交易是一種特殊的交易,它不需要引用之前一筆交易的輸出。它“憑空”產生了幣(也就是產生了新幣),這也是礦工獲得挖出新塊的獎勵,可以理解為“發行新幣”。
在區塊鏈的最初,也就是第一個塊,叫做創世塊。正是這個創世塊,產生了區塊鏈最開始的輸出。對于創世塊,不需要引用之前交易的輸出。因為在創世塊之前根本不存在交易,也就沒有不存在有交易輸出。
來創建一個 coinbase 交易:
~~~
func NewCoinbaseTX(to, data string) *Transaction {
if data == "" {
data = fmt.Sprintf("Reward to '%s'", to)
}
txin := TXInput{[]byte{}, -1, data}//輸入結構:Txid為空,Vout為-1
txout := TXOutput{subsidy, to}//subsidy為獎勵的數額
tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}//只有一個輸出,沒有輸入。
tx.SetID()
return &tx
}
~~~
`subsidy`是獎勵的數額。在比特幣中,實際并沒有存儲這個數字,而是基于區塊總數進行計算而得:區塊總數除以 210000 就是`subsidy`。挖出創世塊的獎勵是 50 BTC,每挖出`210000`個塊后,獎勵減半。在我們的實現中,這個獎勵值將會是一個常量(至少目前是)。
# 將交易保存到區塊鏈
從現在開始,每個塊必須存儲至少一筆交易。如果沒有交易,也就不可能挖出新的塊。這意味著我們應該移除`Block`的`Data`字段,取而代之的是存儲交易:
~~~
type Block struct {
Timestamp int64
Transactions []*Transaction//交易替代data string
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 {//注意,參數是coinbase交易
return NewBlock([]*Transaction{coinbase}, []byte{})
}
~~~
接下來修改創建新鏈的函數:
~~~
func CreateBlockchain(address string) *Blockchain {
...
err = db.Update(func(tx *bolt.Tx) error {
cbtx := NewCoinbaseTX(address, genesisCoinbaseData)//創建一個coinbase交易
genesis := NewGenesisBlock(cbtx)//包含coinbase交易挖出創始區塊
d?:=?genesis.Serialize()
err?=?b.Put(genesis.Hash,?d)?//將創始區塊序列化后插入到數據庫表中
if?err?!=?nil?{
????????????log.Panic(err)
????????}
b, err := tx.CreateBucket([]byte(blocksBucket))
err = b.Put(genesis.Hash, genesis.Serialize())
...
})
...
}
~~~
現在,這個函數會接受一個地址作為參數,這個地址會用來接收挖出創世塊的獎勵。
工作量證明算法必須要將存儲在區塊里面的交易考慮進去,以此保證區塊鏈交易存儲的一致性和可靠性。所以,我們必須修改`ProofOfWork.prepareData`方法:
~~~
func (pow *ProofOfWork) prepareData(nonce int) []byte {
data := bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.HashTransactions(), // 計算交易的哈希,而不再是data string的哈希
IntToHex(pow.block.Timestamp),
IntToHex(int64(targetBits)),
IntToHex(int64(nonce)),
},
[]byte{},
)
return data
}
~~~
計算交易的哈希方法如下:
~~~
func (b *Block) HashTransactions() []byte {
var txHashes [][]byte
var txHash [32]byte
for _, tx := range b.Transactions {//一個block可能有多個交易
txHashes = append(txHashes, tx.ID)//只使用交易ID,不使用交易的輸入和輸出
}
/注意,Join第二個參數是一個空的byte數組,也就是說,連接時候,兩個相鄰的[]byte之間不留間隔符
txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))//將所有交易ID連接起來計算處理的哈希,作為區塊的交易哈希
return txHash[:]
}
~~~
~~~
比特幣使用了一個更加復雜的技術:它將一個塊里面包含的所有交易表示為一個[Merkle tree](https://link.jianshu.com/?t=https://en.wikipedia.org/wiki/Merkle_tree),然后在工作量證明系統中使用樹的根哈希(root hash)。這個方法能夠讓我們快速檢索一個塊里面是否包含了某筆交易,即只需 root hash 而無需下載所有交易即可完成判斷。
~~~
# 未花費的交易輸出
我們需要找到所有的未花費交易輸出(unspent transactions outputs, UTXO)。**未花費(unspent)**指的是這個輸出還沒有被包含在任何交易的輸入中,或者說沒有被任何輸入引用。
當然了,當我們檢查余額時,一般是指那些我們能夠解鎖的那些 UTXO(目前我們還沒有實現密鑰,所以我們將會使用用戶定義的地址來代替,所以這里查詢的是某個地址的余額)。首先,讓我們定義在輸入和輸出上的鎖定和解鎖方法:
~~~
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
return in.ScriptSig == unlockingData
}
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
return out.ScriptPubKey == unlockingData
}
~~~
首先,讓我們定義在輸入和輸出上的解鎖方法:
~~~
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
return in.ScriptSig == unlockingData
}
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
return out.ScriptPubKey == unlockingData
}
~~~
在這里,我們只是將 script 字段與`unlockingData`進行了比較(能解鎖,說明交易與其有關)。在后續文章我們基于私鑰實現了地址以后,會對這部分進行改進。
下一步,找到包含未花費輸出的交易:
~~~
//查詢address地址的未花費輸出的交易
//如果一個輸出被一個地址鎖定,并且這個地址恰好是我們要找的未花費交易輸出的地址,那么這個輸出就是我們想要的。
//不過在獲取它之前,我們需要檢查該輸出是否已經被包含在一個輸入中,也就是檢查它是否已經被花費了:
//由于交易被存儲在區塊里,所以我們不得不檢查區塊鏈里的每一筆交易。從輸出開始:
func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
var unspentTXs []Transaction//未花費交易(輸出還沒包含到其它的交易的輸入之中)
spentTXOs := make(map[string][]int)//已花費交易輸出(查詢所有交易輸入可獲得)
//查詢獲得所有已花費輸出:spentTXOs
bci := bc.Iterator()
for {//查詢區塊鏈中所有區塊
block := bci.Next()
for _, tx := range block.Transactions {//查詢區塊里面的所有交易
txID := hex.EncodeToString(tx.ID)
if tx.IsCoinbase() == false {//如果不是創始區塊交易(創始區塊沒有輸入)
for _, in := range tx.Vin {//查詢交易中的每一個輸入
if in.CanUnlockOutputWith(address) {//address地址是否可以解鎖輸入中的輸入
inTxID := hex.EncodeToString(in.Txid)
spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)//可以解鎖,將當前交易加入到已花費交易輸出數組中
}
}
}
}
if len(block.PrevBlockHash) == 0 {//已經循環結束:創始區塊的PrevBlockHash=[]byte{)
break
}
}
//查詢獲得所有為花費交易:unspentTXs
bci = bc.Iterator()
for {//查詢區塊鏈中所有區塊
block := bci.Next()
for _, tx := range block.Transactions {//查詢區塊里面的所有交易
txID := hex.EncodeToString(tx.ID)
Outputs:
for outIdx, out := range tx.Vout {//查詢交易中的每一個輸出
// 輸出已經花費了嗎?
if spentTXOs[txID] != nil {
for _, spentOut := range spentTXOs[txID] {//如果交易ID在已花費支出map中已經存在,說明已經花費了,檢查下一個輸出
if spentOut == outIdx {
continue Outputs//返回到前面的Outputs位置
}
}
}
//該輸出沒有出現在已花費支出中,加入到未花費交易輸出數組中
if out.CanBeUnlockedWith(address) {//address地址是否可以解鎖輸出中的輸出
unspentTXs = append(unspentTXs, *tx)//可以解鎖,將當前交易加入到未花費交易輸出數組中
}
}
if len(block.PrevBlockHash) == 0 {//已經循環結束:創始區塊的PrevBlockHash=[]byte{)
break
}
}
return unspentTXs//返回未花費交易
}
~~~
# 獲得未花費輸出
將未花費交易作為輸入,然后僅返回一個輸出:
~~~
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
}
~~~
# 通過未花費輸出計算賬戶余額
~~~
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)
}
~~~
#轉賬
將金錢從一個賬戶轉移到另外一個賬戶,我們需要創建一筆新的交易,將它放到一個塊里,然后挖出這個塊。之前我們只實現了 coinbase 交易(這是一種特殊的交易),現在我們需要一種通用的交易:
~~~
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
var inputs []TXInput
var outputs []TXOutput
//validOutputs為sender為此交易提供的輸出,不一定是sender的全部輸出
//acc為sender發出的全部幣數
acc, validOutputs := bc.FindSpendableOutputs(from, amount)
if acc < amount {
log.Panic("ERROR: 沒有交易所需的足夠的錢")
}
// 建立輸入參數
for txid, outs := range validOutputs {
txID, err := hex.DecodeString(txid)
for _, out := range outs {
input := TXInput{txID, out, from}
inputs = append(inputs, input)
}
}
// 建立輸出參數
outputs = append(outputs, TXOutput{amount, to})//款項發給收款人
if acc > amount {
outputs = append(outputs, TXOutput{acc - amount, from}) //找零,退給轉賬人
}
tx := Transaction{nil, inputs, outputs}//初始交易ID設為nil
tx.SetID()//緊接著設置交易的ID
return &tx
}
~~~
轉賬方法:
~~~
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("轉賬成功!")
}
~~~
# 程序執行驗證
## 創建新的區塊鏈

## 獲得余額

## 轉賬

## 打印區塊鏈

- 重要更新說明
- linechain發布
- linechain新版設計
- 引言一
- 引言二
- 引言三
- vs-code設置及開發環境設置
- BoltDB數據庫應用
- 關于Go語言、VS-code的一些Tips
- 區塊鏈的架構
- 網絡通信與區塊鏈
- 單元測試
- 比特幣腳本語言
- 關于區塊鏈的一些概念
- 區塊鏈組件
- 區塊鏈第一版:基本原型
- 區塊鏈第二版:增加工作量證明
- 區塊鏈第三版:持久化
- 區塊鏈第四版:交易
- 區塊鏈第五版:實現錢包
- 區塊鏈第六版:實現UTXO集
- 區塊鏈第七版:網絡
- 階段小結
- 區塊鏈第八版:P2P
- P2P網絡架構
- 區塊鏈網絡層
- P2P區塊鏈最簡體驗
- libp2p建立P2P網絡的關鍵概念
- 區塊鏈結構層設計與實現
- 用戶交互層設計與實現
- 網絡層設計與實現
- 建立節點發現機制
- 向區塊鏈網絡請求區塊信息
- 向區塊鏈網絡發布消息
- 運行區塊鏈
- LineChain
- 系統運行流程
- Multihash
- 區塊鏈網絡的節點發現機制深入探討
- DHT
- Bootstrap
- 連接到所有引導節點
- Advertise
- 搜索其它peers
- 連接到搜到的其它peers
- 區塊鏈網絡的消息訂發布-訂閱機制深入探討
- LineChain:適用于智能合約編程的腳本語言支持
- LineChain:解決分叉問題
- LineChain:多重簽名
- libp2p升級到v0.22版本
- 以太坊基礎
- 重溫以太坊的樹結構
- 世界狀態樹
- (智能合約)賬戶存儲樹
- 交易樹
- 交易收據樹
- 小結
- 以太坊的存儲結構
- 以太坊狀態數據庫
- MPT
- 以太坊POW共識算法
- 智能合約存儲
- Polygon Edge
- block結構
- transaction數據結構
- 數據結構小結
- 關于本區塊鏈的一些說明
- UML工具-PlantUML
- libp2p介紹
- JSON-RPC
- docker制作:啟動多個應用系統
- Dockerfile
- docker-entrypoint.sh
- supervisord.conf
- docker run
- nginx.conf
- docker基礎操作整理
- jupyter計算交互環境
- git技巧一
- git技巧二
- 使用github項目的最佳實踐
- windows下package管理工具