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)。未使用意味著這些輸出在任何輸入中都未被引用。在上圖中,這些是:
- tx0,輸出1;
- tx1,輸出0;
- tx3,輸出0;
- 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方法。之后,為每個找到的輸出創建一個引用它的輸入。接下來,我們創建兩個輸出:
- 一個與接收器地址鎖定的。這是硬幣實際轉移到其他地址。
- 一個與發件人地址鎖定在一起。這是一個變化。只有在未使用的輸出持有比新事務所需的更多價值時才會創建。記住:輸出是不可分割的。
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
好了目前我們實現了交易功能。缺少:
- 地址。我們還沒有真實的,基于私鑰的地址。
- 獎勵。采礦塊絕對沒有利潤!
- UTXO設置。達到平衡需要掃描整個區塊鏈,當區塊數量很多時可能需要很長時間。此外,如果我們想驗證以后的交易,可能需要很長時間。UTXO集旨在解決這些問題并快速處理交易。
- 內存池。這是交易在打包成塊之前存儲的地方。在我們當前的實現中,一個塊只包含一個事務,而且效率很低。
后續降會實現地址、錢包、挖礦獎勵、網絡等。
5.比特幣交易示例總結
交易 | 目的 | 輸入 | 輸出 | 簽名 | 差額 |
---|---|---|---|---|---|
T0 | A轉給B | 他人向A交易的輸出 | B賬號可以使用該交易 | A簽確認 | 輸入減去輸出,為交易服務費 |
T1 | B轉給C | T0的輸出 | C賬戶可以使用該交易 | B簽名確認 | 輸入減去輸出,為交易服務費 |
··· | X轉給Y | 他人向X交易的輸出 | Y賬戶可以使用該交易 | X簽名確認 | 輸入減去輸出,為交易服務費 |
這就是簡單交易流程,我們以上代碼并沒有實現挖礦獎勵。后續將實現錢包,優化查詢交易,挖礦獎勵、網絡。
資料
- 原文來源:https://jeiwan.cc/posts/building-blockchain-in-go-part-4/
- 本文源碼:https://github.com/Even521/study-bitcion-go/tree/part4
- java學習:http://www.lxweimin.com/p/66c065018c7a
- 區塊鏈基礎視頻學習:https://www.bilibili.com/video/av19620321/
- 區塊鏈測試demo:https://anders.com/blockchain/blockchain.html
- 區塊鏈QQ交流群:489512556