教你用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 要做的事情,隨後,對於每個找到的輸出,會創建一個引用該輸出的輸入。接下來,我們創建兩個輸出:

  1. 一個由接收者地址鎖定。這是給其他地址實際轉移的幣。
  2. 一個由發送者地址鎖定。這是一個找零。隻有當未花費輸出超過新交易所需時產生。記住:輸出是不可再分的。
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!

推薦閱讀: