教你用go語言實現比特幣交易功能(Transaction)
比特幣交易
交易(transaction)是比特幣的核心所在,而區塊鏈唯一的目的,也正是為瞭能夠安全可靠地存儲交易。在區塊鏈中,交易一旦被創建,就沒有任何人能夠再去修改或是刪除它。
對於每一筆新的交易,它的輸入會引用(reference)之前一筆交易的輸出(這裡有個例外,coinbase 交易),引用就是花費的意思。所謂引用之前的一個輸出,也就是將之前的一個輸出包含在另一筆交易的輸入當中,就是花費之前的交易輸出。交易的輸出,就是幣實際存儲的地方。下面的圖示闡釋瞭交易之間的互相關聯:
註意:
有一些輸出並沒有被關聯到某個輸入上
一筆交易的輸入可以引用之前多筆交易的輸出
一個輸入必須引用一個輸出
貫穿本文,我們將會使用像“錢(money)”,“幣(coin)”,“花費(spend)”,“發送(send)”,“賬戶(account)” 等等這樣的詞。但是在比特幣中,其實並不存在這樣的概念。交易僅僅是通過一個腳本(script)來鎖定(lock)一些值(value),而這些值隻可以被鎖定它們的人解鎖(unlock)。
每一筆比特幣交易都會創造輸出,輸出都會被區塊鏈記錄下來。給某個人發送比特幣,實際上意味著創造新的 UTXO 並註冊到那個人的地址,可以為他所用。
交易的主函數:
func (cli *CLI) send(from, to string, amount int, nodeID string, mineNow bool) { if !ValidateAddress(from) { log.Panic("ERROR: Sender address is not valid") } if !ValidateAddress(to) { log.Panic("ERROR: Recipient address is not valid") } bc := NewBlockchain(nodeID) //獲取區塊鏈實例 UTXOSet := UTXOSet{bc} //創建UTXO集 defer bc.Db.Close() wallets, err := NewWallets(nodeID) if err != nil { log.Panic(err) } wallet := wallets.GetWallet(from) tx := NewUTXOTransaction(&wallet, to, amount, &UTXOSet) if mineNow { cbTx := NewCoinbaseTX(from, "") txs := []*Transaction{cbTx, tx} newBlock := bc.MineBlock(txs) UTXOSet.Update(newBlock) } else { sendTx(knownNodes[0], tx) } fmt.Println("Success!") }
我們從頭分析整個交易過程,首先利用ValidateAddress()方法判斷輸入的地址是否為有效的比特幣地址,然後從我們的blotDB數據庫中獲取blockchain實例(我們利用一個數據庫實現區塊鏈數據的存儲,這裡讀者可以忽略),其中讀取數據庫的代碼如下
func NewBlockchain(nodeID string) *Blockchain { dbFile := fmt.Sprintf(dbFile, nodeID) if dbExists(dbFile) == false { fmt.Println("No existing blockchain found. Create one first.") os.Exit(1) } var tip []byte db, err := bolt.Open(dbFile, 0600, nil) //打開數據庫 if err != nil { log.Panic(err) } err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) tip = b.Get([]byte("l")) //讀取最新的區塊鏈 return nil }) if err != nil { log.Panic(err) } bc := Blockchain{tip, db} return &bc }
其中我們的區塊鏈的基本原型為
type Blockchain struct { tip []byte Db *bolt.DB } type Block struct { Timestamp int64 Transactions []*Transaction PrevBlockHash []byte Hash []byte Nonce int Height int }
獲取完成區塊鏈實例後,我們創建出一個utxo集合,其數據結構為
type UTXOSet struct { Blockchain *Blockchain }
然後我們從錢包文件中獲取我們的錢包集合(wallets),接著調用我們的轉賬函數。
func NewUTXOTransaction(wallet *Wallet, to string, amount int, UTXOSet *UTXOSet) *Transaction { var inputs []TXInput var outputs []TXOutput pubKeyHash := HashPubKey(wallet.PublicKey) acc, validOutputs := UTXOSet.FindSpendableOutputs(pubKeyHash, 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, nil, wallet.PublicKey} inputs = append(inputs, input) } } // Build a list of outputs from := fmt.Sprintf("%s", wallet.GetAddress()) outputs = append(outputs, *NewTXOutput(amount, to)) //創建新的交易輸出 if acc > amount { outputs = append(outputs, *NewTXOutput(acc-amount, from)) // a change //找零輸出 } tx := Transaction{nil, inputs, outputs} tx.ID = tx.Hash() //創建一筆交易 UTXOSet.Blockchain.SignTransaction(&tx, wallet.PrivateKey) //對交易簽名 return &tx }
對於一筆交易來說,其數據結構為
type Transaction struct { ID []byte Vin []TXInput Vout []TXOutput } type TXInput struct { Txid []byte Vout int Signature []byte PubKey []byte } type TXOutput struct { Value int PubKeyHash []byte } type UTXOSet struct { Blockchain *Blockchain }
一筆交易來說,輸出主要包含兩部分: 一定量的比特幣(Value), 一個鎖定腳本(ScriptPubKey),要花這筆錢,必須要解鎖該腳本。一個輸入引用瞭之前交易的一個輸出:Txid 存儲的是之前交易的 ID,Vout 存儲的是該輸出在那筆交易中所有輸出的索引(因為一筆交易可能有多個輸出,需要有信息指明是具體的哪一個)Signature是簽名,而Pubkey是公鑰,兩者保證瞭用戶無法花費屬於其他人的幣。
func HashPubKey(pubKey []byte) []byte { // RIPEMD160(SHA256(PubKey)) publicSHA256 := sha256.Sum256(pubKey) RIPEMD160Hasher := ripemd160.New() _, err := RIPEMD160Hasher.Write(publicSHA256[:]) if err != nil { log.Panic(err) } publicRIPEMD160 := RIPEMD160Hasher.Sum(nil) return publicRIPEMD160 } func (u UTXOSet) FindSpendableOutputs(pubkeyHash []byte, amount int) (int, map[string][]int) { unspentOutputs := make(map[string][]int) //為輸出開辟一塊內存空間 accumulated := 0 db := u.Blockchain.db //獲取存取區塊鏈的數據庫 err := db.View(func(tx *bolt.Tx) error { //讀取數據庫 b := tx.Bucket([]byte(utxoBucket)) c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { //遍歷數據庫 txID := hex.EncodeToString(k) outs := DeserializeOutputs(v) for outIdx, out := range outs.Outputs { if out.IsLockedWithKey(pubkeyHash) && accumulated < amount { //如果能夠解鎖輸出,代表utxo集中的輸出是的所有者是該公鑰所對應的人 accumulated += out.Value //累加值 unspentOutputs[txID] = append(unspentOutputs[txID], outIdx) //加到數組中 } } } return nil }) if err != nil { log.Panic(err) } return accumulated, unspentOutputs } func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool { //判斷輸出是否能夠被某個公鑰解鎖 return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0 } func NewTXOutput(value int, address string) *TXOutput { txo := &TXOutput{value, nil} //註冊一個輸出 txo.Lock([]byte(address)) //設置輸出的pubhashkey return txo } func (out *TXOutput) Lock(address []byte) { pubKeyHash := Base58Decode(address) pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4] out.PubKeyHash = pubKeyHash }
在創建新的輸出時,我們必須找到所有的為花費的輸出,並且確保他們有足夠的價值(value),這就是FindSpendableOutputs
要做的事情,隨後,對於每個找到的輸出,會創建一個引用該輸出的輸入。接下來,我們創建兩個輸出:
- 一個由接收者地址鎖定。這是給其他地址實際轉移的幣。
- 一個由發送者地址鎖定。這是一個找零。隻有當未花費輸出超過新交易所需時產生。記住:輸出是不可再分的。
func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) { prevTXs := make(map[string]Transaction) for _, vin := range tx.Vin { prevTX, err := bc.FindTransaction(vin.Txid) if err != nil { log.Panic(err) } prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX } tx.Sign(privKey, prevTXs) } func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {//方法接受一個私鑰和之前一個交易的map if tx.IsCoinbase() { return }//判斷是是否為發幣交易,因為發幣交易沒有輸入,故不用進行簽名 for _, vin := range tx.Vin { if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil { log.Panic("ERROR: Previous transaction is not correct") } } txCopy := tx.TrimmedCopy() //將會被簽名的是修剪後的交易副本,而不是一個完整的交易 for inID, vin := range txCopy.Vin { prevTx := prevTXs[hex.EncodeToString(vin.Txid)] txCopy.Vin[inID].Signature = nil txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash //迭代副本中的每一個輸入,在每個輸入中,Pubkey 被設置為所引用輸出的PubKeyHash / dataToSign := fmt.Sprintf("%x\n", txCopy) r, s, err := ecdsa.Sign(rand.Reader, &privKey, []byte(dataToSign))//我們通過private對txCopy進行簽名將這串數字連接起來儲存在signature中 if err != nil { log.Panic(err) } signature := append(r.Bytes(), s.Bytes()...) tx.Vin[inID].Signature = signature txCopy.Vin[inID].PubKey = nil } } func (tx *Transaction) TrimmedCopy() Transaction { var inputs []TXInput var outputs []TXOutput for _, vin := range tx.Vin {//將輸入的TXInput.Signature 和TXIput.PubKey設置為空 inputs = append(inputs, TXInput{vin.Txid, vin.Vout, nil, nil}) } for _, vout := range tx.Vout { outputs = append(outputs, TXOutput{vout.Value, vout.PubKeyHash}) } txCopy := Transaction{tx.ID, inputs, outputs} return txCopy }
交易必須被簽名,因為這是保證發送方不會花費其他人的幣的唯一方式,如果一個簽名是無效的,那麼這筆交易也會被認為是無效的,因為這筆交易無法被加到區塊鏈中。考慮到交易解鎖的是之前的輸出,然後重新分配裡面的價值,並鎖定新的輸出,那麼必須要簽名一下的數據
- 存儲在已經解鎖輸出的公鑰哈希,他識別瞭一筆交易的發送方
- 存儲在新的鎖定輸出裡面的公鑰哈希,他識別瞭一筆交易的接收方
- 新的輸出值
因此,在比特幣裡,所簽名的並不是一個交易,而是一個去除部分簽名的輸入的副本,輸入裡面存儲瞭被引用輸出的ScriptPubKey
如果現在進行過挖礦
cbTx := NewCoinbaseTX(from, "") txs := []*Transaction{cbTx, tx} newBlock := bc.MineBlock(txs) UTXOSet.Update(newBlock) func NewCoinbaseTX(to, data string) *Transaction { if data == "" { //如果數據為空生成一個隨機數據 randData := make([]byte, 20) _, err := rand.Read(randData) if err != nil { log.Panic(err) } data = fmt.Sprintf("%x", randData) }//生成一筆挖礦交易 txin := TXInput{[]byte{}, -1, nil, []byte(data)} txout := NewTXOutput(subsidy, to) tx := Transaction{nil, []TXInput{txin}, []TXOutput{*txout}} tx.ID = tx.Hash() return &tx } func (bc *Blockchain) MineBlock(transactions []*Transaction) *Block { //開始挖礦 var lastHash []byte var lastHeight int for _, tx := range transactions { // TODO: ignore transaction if it's not valid if bc.VerifyTransaction(tx) != true { log.Panic("ERROR: Invalid transaction") //對打包在區塊中的交易進行認證 } } err := bc.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) lastHash = b.Get([]byte("l")) //獲取最新的一個塊的hash值 blockData := b.Get(lastHash) block := DeserializeBlock(blockData) //將最新的一個塊解序列 lastHeight = block.Height return nil }) if err != nil { log.Panic(err) } newBlock := NewBlock(transactions, lastHash, lastHeight+1) 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 }) if err != nil { log.Panic(err) } return newBlock } func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool { if tx.IsCoinbase() { return true } prevTXs := make(map[string]Transaction) for _, vin := range tx.Vin { prevTX, err := bc.FindTransaction(vin.Txid) if err != nil { log.Panic(err) } prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX } return tx.Verify(prevTXs) } func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool { if tx.IsCoinbase() { //判斷是否為大筆交易 return true } for _, vin := range tx.Vin { if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil { log.Panic("ERROR: Previous transaction is not correct") //判斷輸入地址的有效性 } } txCopy := tx.TrimmedCopy() //創建一個裁剪版本的交易副本 curve := elliptic.P256() //我們需要相同區塊用於生成密鑰對 for inID, vin := range tx.Vin { prevTx := prevTXs[hex.EncodeToString(vin.Txid)] txCopy.Vin[inID].Signature = nil txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash r := big.Int{} s := big.Int{} sigLen := len(vin.Signature) r.SetBytes(vin.Signature[:(sigLen / 2)]) s.SetBytes(vin.Signature[(sigLen / 2):]) x := big.Int{} y := big.Int{} keyLen := len(vin.PubKey) x.SetBytes(vin.PubKey[:(keyLen / 2)]) y.SetBytes(vin.PubKey[(keyLen / 2):]) //這裡我們解包存儲在 TXInput.Signature 和 TXInput.PubKey 中的值,因為一個簽名就是一對數字,一個公鑰就是一對坐標。我們之前為瞭存儲將它們連接在一起,現在我們需要對它們進行解包在 crypto/ecdsa 函數中使用 dataToVerify := fmt.Sprintf("%x\n", txCopy) rawPubKey := ecdsa.PublicKey{curve, &x, &y} if ecdsa.Verify(&rawPubKey, []byte(dataToVerify), &r, &s) == false { //驗證 return false } txCopy.Vin[inID].PubKey = nil } return true } func NewBlock(transactions []*Transaction, prevBlockHash []byte, height int) *Block {//產生一個新的塊 block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0, height}//定義數據結構 pow := NewProofOfWork(block) //定義工作量證明的數據結構 nonce, hash := pow.Run() //挖礦 block.Hash = hash[:] block.Nonce = nonce return block } func (pow *ProofOfWork) Run() (int, []byte) { var hashInt big.Int var hash [32]byte nonce := 0 fmt.Printf("Mining a new block") for nonce < maxNonce { data := pow.prepareData(nonce) hash = sha256.Sum256(data) fmt.Printf("\r%x", hash) hashInt.SetBytes(hash[:]) if hashInt.Cmp(pow.target) == -1 { break } else { nonce++ } } fmt.Print("\n\n") return nonce, hash[:] } func (pow *ProofOfWork) prepareData(nonce int) []byte { data := bytes.Join( [][]byte{ pow.block.PrevBlockHash, pow.block.HashTransactions(), IntToHex(pow.block.Timestamp), IntToHex(int64(targetBits)), IntToHex(int64(nonce)), }, []byte{}, ) return data } func (u UTXOSet) Update(block *Block) { db := u.Blockchain.db err := db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(utxoBucket)) for _, tx := range block.Transactions { if tx.IsCoinbase() == false { for _, vin := range tx.Vin { updatedOuts := TXOutputs{} outsBytes := b.Get(vin.Txid) outs := DeserializeOutputs(outsBytes) for outIdx, out := range outs.Outputs { if outIdx != vin.Vout { updatedOuts.Outputs = append(updatedOuts.Outputs, out) } } if len(updatedOuts.Outputs) == 0 { err := b.Delete(vin.Txid) if err != nil { log.Panic(err) } } else { err := b.Put(vin.Txid, updatedOuts.Serialize()) if err != nil { log.Panic(err) } } } } newOutputs := TXOutputs{} for _, out := range tx.Vout { newOutputs.Outputs = append(newOutputs.Outputs, out) } err := b.Put(tx.ID, newOutputs.Serialize()) if err != nil { log.Panic(err) } } return nil }) if err != nil { log.Panic(err) } }
參考
https://jeiwan.cc/
到此這篇關於利用go語言實現比特幣交易(Transaction)的文章就介紹到這瞭,更多相關go語言比特幣交易內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- python區塊鏈簡易版交易完善挖礦獎勵示例
- python區塊鏈簡易版交易實現示例
- go語言實戰之實現比特幣地址校驗步驟
- GO語言創建錢包並遍歷錢包(wallet)的實現代碼
- python區塊鏈基本原型簡版實現示例