title: Golang實現區塊鏈(四)—交易
tags: go,blockchainweb
到目前爲止咱們已經實現了區塊鏈的持久化和交互界面。可是比特幣中最核心的交易功能,咱們還沒能實現它,本章就將對區塊鏈交易功能進行實現。算法
在實現交易功能以前,咱們首先了解下比特幣的交易原理,比特幣採用的是 UTXO 模型,並不是帳戶模型,並不直接存在「餘額」這個概念,餘額須要經過遍歷整個交易歷史得來。數據庫
咱們日常使用支付寶、微信錢包等都是帳戶模型,它們都有相似的兩張表,account和transaction。account表用於存放用戶信息和餘額,而金額交易記錄會存在transaction表。數組
UTXO 是 Unspent Transaction Output(未消費交易輸出),UTXO是中本聰最先在比特幣中採用的一個具體的技術方案。微信
在比特幣的設計中,並無帳戶的概念,若是你想要知道本身的比特幣錢包中多少個比特幣,UTXO的思路是:app
查看有多少筆交易給你比特幣而且你尚未花費掉,你沒有花費掉的總額就是你帶餘額。svg
上面那段話理解起來是否是很繞?別急咱們慢慢來講。函數
花費是什麼概念,每個Transaction Output都猶如現實中的一張紙幣,他只有兩種狀態,屬於你或者不屬於你。
未花費就是該張紙幣屬於你,已花費就是該張紙幣不屬於你。學習
咱們用現實中的例子抽象一下:區塊鏈
按照UTXO來,你的餘額就是45+40 =85。
在剛剛到例子中,咱們存在着找零的現象。那麼在比特幣中是否也存在呢?沒錯比特幣中也存在找零這個概念。可是這個找零概念跟咱們理解的又有點不一樣。
其實並不僅是找零,若是用兜裏一把零碎utxo去轉帳,反而是找回一個整的。
其實這個就至關於咱們現實生活中的找商家換錢,咱們家裏常常攢着一堆硬幣,因此咱們想找個商家給它們換成面值大的紙幣,但又很差意思讓商家直接換,因此會購買某個廉價的商品,而後順便提取換幣的要求。商家接過你的硬幣,並減去你剛剛消費的,會把你剩下了的錢,給你換成面值大的紙幣。 這就是UTXO一個抽象的找零過程。
上面的例子中,父母給錢和買東西,表明比特幣代碼中的input和output。 父母給你錢這筆錢對他們來講就是output(輸出),對你而言就是input(輸入)。
對於每一筆新的交易,它的輸入會引用以前一筆交易的輸出(這裏有個例外,coinbase 交易,coinbase是創世區塊生成的,因此它沒有輸出),引用就是花費的意思。所謂引用以前的一個輸出,也就是將以前的一個輸出包含在另外一筆交易的輸入當中,就是花費以前的交易輸出。交易的輸出,就是幣實際存儲的地方。下面的圖示闡釋了交易之間的互相關聯:
注意:
1.有一些輸出並無被關聯到某個輸入上
2.一筆交易的輸入能夠引用以前多筆交易的輸出
3.一個輸入必須引用一個輸出
一筆交易由一些輸入(input)和輸出(output)組合而來:
type Transaction struct{ // tx hash(交易的惟一標識) TxHash []byte // 輸入 Vins []*TxInput // 輸出 Vouts []*TxOutput }
在比特幣中交易輸出主要包含兩個部分:
1.必定的比特幣,用來消費的錢
2.一個鎖定腳本,要花這筆錢,必須解鎖這個腳本
// 交易輸出 type TxOutput struct { // 1. 有多少錢(金額) Value int64 // 2. 錢是誰的(用戶名) ScriptPubkey string } }
實際上,正是輸出裏面存儲了「幣」(注意,也就是上面的 Value 字段)。而這裏的存儲,指的是用一個數學難題對輸出進行鎖定,這個難題被存儲在 ScriptPubKey 裏面。在內部,比特幣使用了一個叫作 Script 的腳本語言,用它來定義鎖定和解鎖輸出的邏輯。
因爲咱們尚未實現地址,因此目前 ScriptPubkey 將僅僅存儲一個用戶自定義的任意錢包地址.
在比特幣中交易輸出主要包含三個部分:
1.存儲的是以前交易的hash
2.上一筆交易的output索引
3.解鎖腳本
// 交易輸入 type TxInput struct { // 交易哈希(不是當前交易的哈希) TxHash []byte // 引用的上一筆交易的output索引 Vout int // 用戶名 ScriptSig string }
ScriptSig 是一個腳本,提供了可解鎖輸出結構裏面 ScriptPubKey 字段的數據。若是 ScriptSig 提供的數據是正確的,那麼輸出就會被解鎖,而後被解鎖的值就能夠被用於產生新的輸出;若是數據不正確,輸出就沒法被引用在輸入中,或者說,沒法使用這個輸出。這種機制,保證了用戶沒法花費屬於其餘人的幣。
因爲咱們尚未實現地址,因此目前 ScriptSig 將僅僅存儲一個用戶自定義的任意錢包地址
// 生成交易哈希 func (tx *Transaction) HashTransaction() { var result bytes.Buffer encoder := gob.NewEncoder(&result) err := encoder.Encode(tx) if nil != err { log.Panicf("tx hash generate failed! %v\n", err) } hash := sha256.Sum256(result.Bytes()) tx.TxHash = hash[:] }
前面咱們說過「對於每一筆新的交易,它的輸入會引用以前一筆交易的output(這裏有個例外,coinbase 交易)」 。什麼是coinbase 交易呢?「coinbase transaction」是一種特殊類型的交易,它不須要任何output,他是由創世區塊生成的,因此沒有比它更早的output了。
// 生成coinbase交易 func NewCoinbaseTransaction(address string) *Transaction { // 輸入 txInput := &TxInput{[]byte{}, -1, "Genesis Data"} // 輸出 txOutput := &TxOutput{10, address} txCoinbase := &Transaction{nil,[]*TxInput{txInput}, []*TxOutput{txOutput}} // hash txCoinbase.HashTransaction() return txCoinbase }
一個coinbase交易只能有一個input。在咱們的實現裏,TxHash是空的,Vout是-1。另外,coinbase也不須要存儲ScriptSig。相反,有任意的數據存儲在這裏。
在咱們以前的區塊設計中,用了Data來表明交易信息,如今咱們已經實現了Transaction,而且如今只能經過交易來挖出新的區塊,所以咱們應該用Transaction來替換Data。
// 實現一個最基本的區塊結構 type Block struct { TimeStamp int64 // 區塊時間戳,區塊產生的時間 Heigth int64 // 區塊高度(索引、號碼),表明當前區塊的高度 PrevBlockHash []byte // 前一個區塊(父區塊)的哈希 Hash []byte // 當前區塊的哈希 //Data []byte // 交易數據 Txs []*Transaction // 交易數據 Nonce int64 // 用於生成工做量證實的哈希 }
隨着Block的更改,咱們其餘都代碼也須要進行更改。
// 建立新的區塊 //data被替換成了txs func NewBlock(height int64, prevBlockHash []byte, txs []*Transaction) *Block { var block Block block = Block{Heigth:height,PrevBlockHash:prevBlockHash,Txs:txs,TimeStamp:time.Now().Unix()} //block.SetHash() // 生成區塊當前哈希 pow := NewProofOfWork(&block) hash, nonce := pow.Run() // 解題(執行工做量證實算法) block.Hash = hash block.Nonce = nonce return &block }
更改創世區塊
// 生成創世區塊 func CreateGenesisBlock(txs []*Transaction) *Block { return NewBlock(1,nil,txs) }
添加區塊也要更改
// 添加新的區塊到區塊鏈中 func (bc *BlockChain) AddBlock(txs []*Transaction /*替換參數*/) { // 更新數據 err := bc.DB.Update(func(tx *bolt.Tx) error { // 1 獲取數據表 b := tx.Bucket([]byte(blockTableName)) if nil != b { // 2. 確保表存在 // 3. 獲取最新區塊的哈希 // newEstHash := b.Get([]byte("l")) blockBytes := b.Get(bc.Tip) latest_block := DeserializeBlock(blockBytes) // 4. 建立新區塊 newBlock := NewBlock(latest_block.Heigth + 1, latest_block.Hash, txs) // 建立一個新的區塊 // 5. 存入數據庫 err := b.Put(newBlock.Hash, newBlock.Serialize()) if nil != err { log.Panicf("put the data of new block into db failed! %v\n", err) } // 6. 更新最新區塊的哈希 err = b.Put([]byte("l"), newBlock.Hash) if nil != err { log.Panicf("put the hash of the newest block into db failed! %v\n", err) } bc.Tip = newBlock.Hash } return nil }) if nil != err { log.Panicf("update the db of block failed! %v\n",err) } }
固然我也要更改CLI區塊添加的功能
// 添加區塊 func (cli *CLI) addBlock(txs []*Transaction) { if dbExists() == false { fmt.Println("數據庫不存在...") os.Exit(1) } blockchain := BlockchainObject() // 獲取區塊鏈對象 defer blockchain.DB.Close() blockchain.AddBlock(txs) }
咱們前面說過,Coinbase Transaction是創世區塊產生的,因此咱們要在區塊鏈初始化中添加NewCoinbaseTransaction函數,而且從新設置創世區塊的生成。
// 初始化區塊鏈 func CreateBlockChainWithGenesisBlock(address string) *BlockChain { if dbExists() { fmt.Println("創世區塊已存在...") os.Exit(1) // 退出 } // 建立或者打開數據 db, err := bolt.Open(dbName, 0600,nil) if nil != err { log.Panicf("open the db failed! %v\n", err) } //defer db.Close() var blockHash []byte // 須要存儲到數據庫中的區塊哈希 err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if nil == b { // 添加創世區塊 b, err = tx.CreateBucket([]byte(blockTableName)) if nil != err { log.Panicf("create the bucket [%s] failed! %v\n", blockTableName, err) } } if nil != b { // 生成交易 txCoinbase := NewCoinbaseTransaction(address) // 生成創世區塊( genesisBlock := CreateGenesisBlock([]*Transaction{txCoinbase}) err = b.Put(genesisBlock.Hash, genesisBlock.Serialize()) if nil != err { log.Panicf("put the data of genesisBlock to db failed! %v\n", err) } // 存儲最新區塊的哈希 err = b.Put([]byte("l"), genesisBlock.Hash) if nil != err { log.Panicf("put the hash of latest block to db failed! %v\n", err) } blockHash = genesisBlock.Hash } return nil }) if nil != err { log.Panicf("update the data of genesis block failed! %v\n", err) } return &BlockChain{db, blockHash} }
回想下咱們以前的代碼,在進行工做量證實以前,咱們在準備的數據中加入了pow.Block.Data。如今咱們已經使用了Txs, 可是由於Txs是結構體,不是咱們須要的[]byte,因此咱們如今把區塊中的全部交易結構轉換成[]byte。
// 把區塊中的全部交易結構轉換成[]byte func (block *Block) HashTransactions() []byte { var txHashes [][]byte for _, tx := range block.Txs { txHashes = append(txHashes,tx.TxHash) } // sha256 txHash := sha256.Sum256(bytes.Join(txHashes, []byte{})) return txHash[:] }
如今咱們來替換掉pow.Block.Data
// 準備數據,將區塊相差屬性搭接越來,返回一個字節數組 func (pow *ProofOfWork) prepareData(nonce int) []byte { data := bytes.Join([][]byte{ pow.Block.PrevBlockHash, //pow.Block.Data; pow.Block.HashTransactions(), IntToHex(pow.Block.TimeStamp), IntToHex(pow.Block.Heigth), IntToHex(int64(nonce)), IntToHex(targetBit), },[]byte{}) return data }
// 生成轉帳交易 func NewSimpleTransaction(from string, to string, amount int) *Transaction { var txInputs []*TxInput // 輸入 var txOutputs []*TxOutput // 輸出 /* type TxInput struct { // 交易哈希(不是當前交易的哈希) TxHash []byte // 引用的上一筆交易的output索引 Vout int // 用戶名 ScriptSig string } */ // 消費 txInput := &TxInput{[]byte("8ae02501631d68b4bcab27ed41105d0b751dfd057443c9f8a68e126db686e9ed"), 0, from} txInputs = append(txInputs, txInput) // 轉帳 txOutput := &TxOutput{int64(amount), to} txOutputs = append(txOutputs, txOutput) // 找零 txOutput = &TxOutput{5 - int64(amount), from} txOutputs = append(txOutputs, txOutput) // 生成交易 tx := &Transaction{nil, txInputs, txOutputs} tx.HashTransaction() return tx }
// 返回指定地址的餘額 func UnUTXOS(address string) []*TxOutput { fmt.Printf("the address is %s\n", address) return nil }
經過CLI獲取餘額
// 查詢餘額 func (cli *CLI) getBalance(from string) { // 獲取指定地址的餘額 outPuts := UnUTXOS(from) fmt.Printf("unUTXO : %v\n", outPuts) }
要想發送交易,首先咱們要獲取到Blockchain對象
// 返回Blockchain 對象 func BlockchainObject() *BlockChain { // 讀取數據庫 db, err := bolt.Open(dbName, 0600, nil) if nil != err { log.Panicf("get the object of blockchain failed! %v\n", err) } var tip []byte // 最新區塊的哈希值 err = db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if nil != b { tip = b.Get([]byte("l")) } return nil }) return &BlockChain{db, tip} }
如今,咱們要把幣送給其它人。爲了實現這個,須要建立一筆交易,把它設到區塊中,而後挖出這個區塊。到目前爲止,咱們的代碼也只是實現了coinbase交易,如今須要一個普通的交易。
/ 挖礦(生成新的區塊) // 經過接收交易,進行打包確認,最終生成新的區塊 func (blockchain *BlockChain)MineNewBlock(from, to, amount []string) { fmt.Printf("\tFROM:[%s]\n", from) fmt.Printf("\tTO:[%s]\n", to) fmt.Printf("\tAMOUNT:[%s]\n", amount) // 接收交易 var txs []*Transaction // 要打包的交易列表 value, _ := strconv.Atoi(amount[0]) tx := NewSimpleTransaction(from[0], to[0], value) txs = append(txs, tx) // 打包交易 // 生成新的區塊 var block *Block // 從數據庫中獲取最新區塊 blockchain.DB.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if nil != b { hash := b.Get([]byte("l")) // 獲取最新區塊哈希值(看成新生區塊的prevHash) blockBytes := b.Get(hash) // 獲得最新區塊(爲了獲取區塊高度) block = DeserializeBlock(blockBytes) // 反序列化 } return nil }) // 生成新的區塊 block = NewBlock(block.Heigth + 1, block.Hash, txs) // 持久化新區塊 blockchain.DB.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if nil != b { err := b.Put(block.Hash,block.Serialize()) if nil != err { log.Panicf("update the new block to db failed! %v\n", err) } b.Put([]byte("l"), block.Hash) // 更新數據庫中的最新哈希值 blockchain.Tip = block.Hash } return nil }) }
接着咱們在CLI交互中定義 發送方法
// 發送交易 func (cli *CLI) send(from, to, amount []string) { // 檢測數據庫 if dbExists() == false { fmt.Println("數據庫不存在...") os.Exit(1) } blockchain := BlockchainObject() // 獲取區塊鏈對象 defer blockchain.DB.Close() blockchain.MineNewBlock(from, to, amount) }
傳送幣到其它地址,意味着會建立新的交易,而後會經過挖出新的區塊,把交易放到該區塊中,再把該區塊放到區塊鏈的方式讓交易得以在區塊鏈中。可是區塊鏈並不會當即作到這一步,相反,它把全部的交易放到存儲池中,當礦機準備好挖區塊時,它就把存儲池中的全部交易拿出來並建立候選的區塊。交易只有在包含了該交易的區塊被挖出且附加到區塊鏈中時纔會被確認。
// 遍歷輸出區塊鏈全部區塊的信息 func (bc *BlockChain) PrintChain() { fmt.Println("區塊鏈完整信息...") var curBlock* Block ///var currentHash []byte = bc.Tip // 獲取最新區塊的哈希 // 建立一個迭代器對象 bcit := bc.Iterator() for { fmt.Printf("----------------------------------------\n") curBlock = bcit.Next() fmt.Printf("\tHeigth : %d\n", curBlock.Heigth) fmt.Printf("\tTimeStamp : %d\n", curBlock.TimeStamp) fmt.Printf("\tPrevBlockHash : %x\n", curBlock.PrevBlockHash) fmt.Printf("\tHash : %x\n", curBlock.Hash) fmt.Printf("\tTransaction : %v\n", curBlock.Txs) for _, tx := range curBlock.Txs { fmt.Printf("\t\t tx-hash: %x\n", tx.TxHash) fmt.Println("\t\t輸入...") for _, vin := range tx.Vins { fmt.Printf("\t\t\tvin-txhash:%x\n", vin.TxHash) fmt.Printf("\t\t\tvin-vout:%v\n", vin.Vout) fmt.Printf("\t\t\tvin-scriptsig:%v\n", vin.ScriptSig) } fmt.Println("\t\t輸出...") for _, vout := range tx.Vouts { fmt.Printf("\t\t\tvout-value:%d\n", vout.Value) fmt.Printf("\t\t\tvout-ScriptPubkey:%v\n", vout.ScriptPubkey) } } fmt.Printf("\tNonce : %d\n", curBlock.Nonce) // 判斷是否已經遍歷到創世區塊 var hashInt big.Int hashInt.SetBytes(curBlock.PrevBlockHash) if big.NewInt(0).Cmp(&hashInt) == 0 { break // 跳出循環 } //currentHash = curBlock.PrevBlockHash } }
// 運行函數 func (cli *CLI) Run() { // 1. 檢測參數數量 IsValidArgs() // 2. 新建命令 // 添加區塊 addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError) // 打印區塊鏈信息 printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError) // 建立區塊鏈 createBlCWithGenesisCmd := flag.NewFlagSet("createblockchain", flag.ExitOnError) // 發送交易 sendCmd := flag.NewFlagSet("send", flag.ExitOnError) // 查詢餘額 getBalanceCmd := flag.NewFlagSet("getbalance", flag.ExitOnError) // 3. 獲取命令行參數 flagAddBlockArg:=addBlockCmd.String("data","send 100 BTC to everyone","交易數據...") flagCreateBlockchainWithAddress := createBlCWithGenesisCmd.String("address","","地址...") // 轉帳命令行參數 flagFromArg := sendCmd.String("from", "","轉帳地址...") flagToArg := sendCmd.String("to", "", "轉帳目標地址...") flagAmount := sendCmd.String("amount", "", "轉帳金額...") // 查詢餘額命令行參數 flagBalanceArg := getBalanceCmd.String("address", "", "查詢地址...") switch os.Args[1] { case "send": err := sendCmd.Parse(os.Args[2:]) if nil != err { log.Panicf("parse cmd of send failed! %v\n", err) } case "addblock": err := addBlockCmd.Parse(os.Args[2:]) if nil != err { log.Panicf("parse cmd of add block failed! %v\n", err) } case "printchain": err := printChainCmd.Parse(os.Args[2:]) if nil != err { log.Panicf("parse cmd of printchain failed! %v\n", err) } case "createblockchain": err := createBlCWithGenesisCmd.Parse(os.Args[2:]) if nil != err { log.Panicf("parse cmd of create block chain failed! %v\n", err) } case "getbalance": err := getBalanceCmd.Parse(os.Args[2:]) if nil != err { log.Panicf("get balance failed! %v\n", err) } default: PrintUsage() os.Exit(1) } // 添加餘額查詢命令 if getBalanceCmd.Parsed() { if *flagBalanceArg == "" { fmt.Println("未指定查詢地址...") PrintUsage() os.Exit(1) } cli.getBalance(*flagBalanceArg) } // 添加轉帳命令 if sendCmd.Parsed() { if *flagFromArg == "" { fmt.Println("源地址不能爲空...") PrintUsage() os.Exit(1) } if *flagToArg == "" { fmt.Println("目標地址不能爲空...") PrintUsage() os.Exit(1) } if *flagAmount == "" { fmt.Println("金額不能爲空...") PrintUsage() os.Exit(1) } cli.send(JSONToArray(*flagFromArg), JSONToArray(*flagToArg), JSONToArray(*flagAmount)) // 發送交易 } // 添加區塊命令 if addBlockCmd.Parsed() { if *flagAddBlockArg == "" { PrintUsage() os.Exit(1) } cli.addBlock([]*Transaction{}) } // 輸出區塊鏈信息命令 if printChainCmd.Parsed() { cli.printchain() } // 建立區塊鏈 if createBlCWithGenesisCmd.Parsed() { if *flagCreateBlockchainWithAddress == "" { PrintUsage() os.Exit(1) } cli.createBlockchainWithGenesis(*flagCreateBlockchainWithAddress) } }
咱們總算實現了交易功能。儘管關鍵的特性像比特幣那樣的加密貨幣尚未實現:咱們尚未實現真正的地址、挖礦獎勵。