Go實現區塊鏈(四)---交易事物(一)

1.前言

上一篇我知道了區塊鏈如何持久化存儲,接下來我們將開始實現區塊鏈中交易是如何產生的如何防止被串改,如何在網絡中分布式記賬。我們將交易分成兩部分:交易實現一般機制,后面將實現網絡、獎勵機制等。

2.知識準備

知識點 學習網頁 特性
bitcoin交易 交易 不可串改
Coinbase 創世塊交易信息 創世塊交易

3.基本交易過程

在區塊鏈中每次發生交易,用戶需要將新的交易記錄寫到比特幣區塊鏈網絡中,等待網絡確認為交易完成。每個交易包括了一些輸入和一些輸出,未經使用的交易的輸出(Transaction Outputs,UTXO)可以被新的交易引用作為合法輸入,被使用過的交易的輸出(Spent Transaction Outputs,STO)則無法被引用作為合法輸入。

注意:這里的比特幣交易與我們傳統的金錢付款交易是不同的,并沒有賬號、沒有余額等。詳情參考

4.代碼實現

交易:

//交易事物
type Transaction struct {
    ID   []byte     //交易hash
    Vin  []TXInput  //事物輸入
    Vout []TXOutput //事物輸出
}

流程圖:


交易引用圖

注意:

  • 有些輸出和輸入無關。
  • 在一個交易中,投入可以參考多個交易的輸出。
  • 輸入必須引用輸出。

比特幣中沒有這樣的概念。事務只是用腳本鎖定值,只能由鎖定它的人解鎖。

交易輸出:

//一個事物輸出
type TXOutput struct {
    Value int       //值
    ScriptPubKey string //解鎖腳本key
}

實際上,它是存儲“硬幣”的輸出(注意Value上面的字段)。而存儲意味著用一個拼圖鎖定它們,這是存儲在ScriptPubKey。在內部,比特幣使用稱為腳本的腳本語言,用于定義輸出鎖定和解鎖邏輯。這個語言很原始(這是故意的,以避免可能的黑客和濫用),但我們不會詳細討論它。你可以在本章知識點bitcoin交易詳細解釋。

在比特幣中,價值領域存儲satoshis的數量,而不是BTC的數量。甲聰是100000000分之1一個比特幣(0.00000001 BTC)的,因此,這是貨幣的比特幣的最小單位(如百分比)。

由于我們沒有實現地址,現在我們將避免整個腳本相關的邏輯。ScriptPubKey將存儲任意字符串(用戶定義的錢包地址)。

交易輸入:

//一個事物輸入
type TXInput struct {
    Txid []byte //交易ID的hash
    Vout int    //交易輸出
    ScriptSig string //解鎖腳本
}

如前所述,輸入引用前一個輸出:Txid存儲此類事務的ID,并Vout在事務中存儲輸出的索引。ScriptSig是一個提供數據以在輸出中使用的腳本ScriptPubKey。如果數據是正確的,輸出可以被解鎖,并且它的值可以被用來產生新的輸出; 如果不正確,則輸出中不能引用輸出。這是保證用戶不能花錢屬于其他人的硬幣的機制。

同樣,由于我們還沒有實現地址,ScriptSig因此將只存儲任意用戶定義的錢包地址。我們將在下一篇文章中實現公鑰和簽名檢查。

我們總結一下。產出是儲存“硬幣”的地方。每個輸出都帶有一個解鎖腳本,它決定了解鎖輸出的邏輯。每個新事務都必須至少有一個輸入和輸出。輸入引用前一個事務的輸出,并提供ScriptSig輸出的解鎖腳本中使用的數據(字段),以解除鎖定并使用其值創建新的輸出。

coinbase交易:
上面我們知道輸入參考輸出邏輯,而輸出又參考了輸入。這樣就產生了我們常見的一個問題:先有雞還是先有蛋呢?

當礦工開始挖礦時,它會添加一個coinbase交易。coinbase交易是一種特殊類型的交易,不需要以前存在的輸出。它無處不在地創造產出(即“硬幣”)。沒有雞的雞蛋。這是礦工獲得開采新礦區的獎勵。

如您所知,區塊鏈開始處有起始塊。這個區塊在區塊鏈中產生了第一個輸出。由于沒有以前的交易并且沒有這樣的輸出,因此不需要先前的輸出。

我們來創建一個coinbase事務:

//創建Coinbase事物
func NewCoinbaseTX(to,data string) *Transaction  {
    if data==""{
        data=fmt.Sprintf("Reward to '%s'",to)
    }
    //這里Vout-1 data:const genesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
    txin :=TXInput{[]byte{},-1,data}
    //subsidy是獎勵的金額  
    txout := TXOutput{subsidy, to}
    tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
    //設置32位交易hash
    tx.SetID()
    return &tx
}

//設置交易ID hash
func (tx *Transaction) SetID(){
    var encoded bytes.Buffer
    var hash [32]byte //32位的hash字節

    enc := gob.NewEncoder(&encoded)
    err := enc.Encode(tx)
    if err != nil {
        log.Panic(err)
    }
    //將交易信息sha256
    hash = sha256.Sum256(encoded.Bytes())
    //生成hash
    tx.ID = hash[:]
}

一個coinbase交易只有一個輸入。在我們的實現中它Txid是空的,Vout等于-1。另外,coinbase事務不會存儲腳本ScriptSig。相反,任意數據存儲在那里。

在比特幣中,第一個coinbase交易包含以下信息:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。查看知識點Coinbase可以知道。

subsidy是獎勵的金額。在比特幣中,這個數字沒有存儲在任何地方,只根據塊的總數進行計算:塊的數量除以210000。挖掘創世紀塊產生50 BTC,每210000塊獎勵減半。在我們的實現中,我們會將獎勵作為常量存儲(至少現在是??)。

在區塊鏈中存儲交易:
我們將開始區塊里面的data改成transactions

//區塊結構
type Block struct {
    Hash          []byte //hase值
    Transactions []*Transaction//交易數據
    PrevBlockHash []byte //存儲前一個區塊的Hase值
    Timestamp     int64  //生成區塊的時間
    Nonce         int    //工作量證明算法的計數器
}

對應的NewBlock,NewGensisBlock也應該修改:

//生成一個新的區塊方法
func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block{
    //GO語言給Block賦值{}里面屬性順序可以打亂,但必須制定元素 如{Timestamp:time.Now().Unix()...}
    block := &Block{Timestamp:time.Now().Unix(), Transactions:transactions, PrevBlockHash:prevBlockHash, Hash:[]byte{},Nonce:0}

    //工作證明
    pow :=NewProofOfWork(block)
    //工作量證明返回計數器和hash
    nonce, hash := pow.Run()
    block.Hash = hash[:]
    block.Nonce = nonce
    return block
}

//創建并返回創世紀Block
func  NewGenesisBlock(coinbase *Transaction) *Block {
    return NewBlock([]*Transaction{coinbase}, []byte{})
}

blockchain:

/ 創新一個新的區塊數據
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))
        if err != nil {
            log.Panic(err)
        }
        err = b.Put(genesis.Hash, genesis.Serialize())
        ...
}

這里,函數將獲得一個地址,該地址將獲得挖掘創世塊的獎勵。(我們這里獎勵為10)

工作量證明:

Proof-of-Work算法必須考慮存儲在塊中的事務,以保證區塊鏈作為事務存儲的一致性和可靠性。所以現在我們必須修改ProofOfWork.prepareData方法:

//將區塊體里面的數據轉換成一個字節碼數組,為下一個區塊準備數據
func (pow *ProofOfWork) prepareData(nonce int) []byte {
    //注意一定要將原始數據轉換成[]byte,不能直接從字符串轉
    data := bytes.Join(
        [][]byte{
            pow.block.PrevBlockHash,
            pow.block.HashTransactions(),
            utils.IntToHex(pow.block.Timestamp),
            utils.IntToHex(int64(targetBits)),
            utils.IntToHex(int64(nonce)),
        },
        []byte{},
    )
    return data
}

將data改成hashTransactions:

//返回塊狀事務的hash
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[:]
}

我們使用散列作為提供數據的唯一表示的機制。我們希望塊中的所有事務都由一個散列唯一標識。為了達到這個目的,我們得到每個事務的哈希值,連接它們,并獲得連接組合的哈希值。

比特幣使用更復雜的技術:它將所有包含在塊中的事務表示為Merkle樹,并在Proof-of-Work系統中使用樹的根散列。這種方法允許快速檢查塊是否包含某個事務,只有根散列并且不下載所有事務。(后續會詳細講解Merkle算法)

好了我們現在嘗試一下CreateBlockchain:
編譯:

C:\go-worke\src\github.com\study-bitcoin-go>go build github.com/study-bitcoin-go

執行createblockchain命令:

C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createblockchain -address even

輸出:

Dig into mine  00000860adb64e2ca9c83d0f665f7ec3148ec9d32b64cb97f2481712c4d94d79

Done!

目前為止我們實現開采創世塊獎勵。但我們要如何實現查詢余額呢?

未使用交易輸出
我們需要找到所有未使用的交易輸出(UTXO)。未使用意味著這些輸出在任何輸入中都未被引用。在上圖中,這些是:

  1. tx0,輸出1;
  2. tx1,輸出0;
  3. tx3,輸出0;
  4. tx4,輸出0。

當然,當我們檢查余額時,我們不需要所有這些,但只有那些可以用我們擁有的密鑰解鎖的(當前我們沒有實現密鑰并且將使用用戶定義的地址)。首先,我們來定義輸入和輸出上的鎖定 - 解鎖方法:

//通過檢查地址是否啟動了事務
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
    return in.ScriptSig == unlockingData
}
//檢查輸出是否可以使用所提供的數據進行解鎖
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
    return out.ScriptPubKey == unlockingData
}

這里我們只是比較腳本字段unlockingData。在我們實現基于私鑰的地址后,這些作品將在未來的文章中得到改進。
下一步 - 查找包含未使用產出的交易 - 相當困難:

//查詢未處理的事務
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)  //交易ID轉換成string
        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)
                }
            }

            //我們跳過輸入中引用的那些(它們的值被移到其他輸出,因此我們不能計數它們)。
            // 在檢查輸出之后,我們收集所有可能解鎖輸出的輸入,并鎖定提供的地址(這不適用于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)
                    }
                }
            }
        }
        if len(block.PrevBlockHash) == 0 {
            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
}

客戶端cli getbalance命令:

//查詢余額
func (cli *CLI) getBalance(address string) {
    bc := block.NewBlockchain(address)
    defer block.Close(bc)

    balance := 0
    //查詢所有未經使用的交易地址
    UTXOs := bc.FindUTXO(address)
    //算出未使用的交易地址的value和
    for _, out := range UTXOs {
        balance += out.Value
    }
    fmt.Printf("Balance of '%s': %d\n", address, balance)
}

賬戶余額是由賬戶地址鎖定的所有未使用的交易輸出值的總和。

現在我們檢查一下地址為even的錢:
重新編譯后

C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address even
Balance of 'even': 10

這就是第一筆錢,接下來我們需要實現給一個地址轉幣

現在,我們要發送一些硬幣給別人。為此,我們需要創建一個新的事務,將它放在一個塊中,然后挖掘塊。到目前為止,我們只實現了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)
        if err != nil {
            log.Panic(err)
        }
        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
}

在創建新的輸出之前,我們首先必須找到所有未使用的輸出并確保它們存儲足夠的值。這是什么FindSpendableOutputs方法。之后,為每個找到的輸出創建一個引用它的輸入。接下來,我們創建兩個輸出:

  1. 一個與接收器地址鎖定的。這是硬幣實際轉移到其他地址。
  2. 一個與發件人地址鎖定在一起。這是一個變化。只有在未使用的輸出持有比新事務所需的更多價值時才會創建。記住:輸出是不可分割的。

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分組的累計值和輸出索引。我們不想花更多的錢。

現在我們可以修改該Blockchain.MineBlock方法:

//開采區塊
func (bc *Blockchain) MineBlock(transactions []*Transaction)  {
    var lastHash  []byte//最新一個hash
    err := bc.db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        lastHash = b.Get([]byte("l"))

        return nil
    })
    if err != nil {
        log.Panic(err)
    }
    //創造一個新區塊
    newBlock := NewBlock(transactions, lastHash)
    //修改"l"的hash
    err = bc.db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        err := b.Put(newBlock.Hash, newBlock.Serialize())
        if err != nil {
            log.Panic(err)
        }
        err = b.Put([]byte("l"), newBlock.Hash)
        if err != nil {
            log.Panic(err)
        }
        bc.tip = newBlock.Hash

        return nil
    })
}

cli添加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!")
}

發送硬幣意味著創建一個交易并通過挖掘一個塊將其添加到區塊鏈。但比特幣不會立即做到這一點(就像我們一樣)。相反,它將所有新事務放入內存池(或mempool)中,并且當礦工準備開采塊時,它將從mempool獲取所有事務并創建候選塊。交易只有在包含它們的區塊被挖掘并添加到區塊鏈時才會被確認。

現在我們來試試發幣:

C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go send  -from even -to jim -amount 3
 Mining the block containing "S4t?.U?????H??vWE???[?P?╔???"
 Dig into mine  00000cde90398b754eebe6d7820dab6e6260ae724712b72706846ec6d331fe2c

Success!

分別查詢even、tom錢包:

C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address even
Balance of 'even': 7

C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address jim
Balance of 'jim': 3

好了目前我們實現了交易功能。缺少:

  1. 地址。我們還沒有真實的,基于私鑰的地址。
  2. 獎勵。采礦塊絕對沒有利潤!
  3. UTXO設置。達到平衡需要掃描整個區塊鏈,當區塊數量很多時可能需要很長時間。此外,如果我們想驗證以后的交易,可能需要很長時間。UTXO集旨在解決這些問題并快速處理交易。
  4. 內存池。這是交易在打包成塊之前存儲的地方。在我們當前的實現中,一個塊只包含一個事務,而且效率很低。

后續降會實現地址、錢包、挖礦獎勵、網絡等。

5.比特幣交易示例總結

交易 目的 輸入 輸出 簽名 差額
T0 A轉給B 他人向A交易的輸出 B賬號可以使用該交易 A簽確認 輸入減去輸出,為交易服務費
T1 B轉給C T0的輸出 C賬戶可以使用該交易 B簽名確認 輸入減去輸出,為交易服務費
··· X轉給Y 他人向X交易的輸出 Y賬戶可以使用該交易 X簽名確認 輸入減去輸出,為交易服務費

這就是簡單交易流程,我們以上代碼并沒有實現挖礦獎勵。后續將實現錢包,優化查詢交易,挖礦獎勵、網絡。

資料

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容