歡迎光臨
每天分享高質量文章

通過blockchain_go分析區塊鏈交易原理

1.背

在去中心化的區塊鏈中進行交易(轉賬)是怎麼實現的呢?本篇通過blockchain_go來分析一下。需要進行交易,首先就需要有交易的雙方以及他們的認證機制,其次是各自的資金賬戶規則。在分佈式賬本系統裡面,需要有機制能夠準確驗證一個用戶身份以及對賬戶資金的精確計算,不能出現一丁點差錯。在區塊鏈中交易通過Transaction表示,而賬戶的自己並不是在每個節點上儲存每個用戶的一個餘額的數字,而是通過歷史交易信息計算而來(歷史交易不可篡改),其中的關鍵機制是UTXO。

2.身份認證

在區塊鏈身份認證是採用RSA非對稱加密體系完成,每個用戶在會擁有一個“錢包”,錢包是通過安全的橢圓曲線加密演算法生成,其中包括一對公私鑰。私鑰自己保留不能暴露,用作加密,簽名等,公鑰公開給所有人,用於信息驗證等。只要是用私鑰簽名的信息,就可以通過配對的公鑰解碼認證,不可抵賴。在blockchain_go中,錢包實現如下:

// Wallet stores private and public keystype Wallet struct {    PrivateKey ecdsa.PrivateKey    PublicKey  []byte}// NewWallet creates and returns a Walletfunc NewWallet() *Wallet {    private, public := newKeyPair()    wallet := Wallet{private, public}    return &wallet; }func newKeyPair() (ecdsa.PrivateKey, []byte) {    curve := elliptic.P256() //橢圓曲線    private, err := ecdsa.GenerateKey(curve, rand.Reader) //生成私鑰    if err != nil {        log.Panic(err)    }    pubKey := append(private.PublicKey.X.Bytes(),private.PublicKey.Y.Bytes()...) //合成公鑰    return *private, pubKey }

錢包最重要的功能就是為用戶提供身份認證和加解密的公私鑰對。

3.什麼是Transaction

區塊鏈中的Transaction(交易)就是一批輸入和輸出的集合,比如A通過交易給B10個代幣(token),那麼交易就是A輸入10代幣,輸出變成B得到10代幣,這樣A就減少10代幣,B增加10代幣,再將這個交易信息儲存到區塊鏈中固化後,A和B在區塊鏈中的賬號狀態就發生了永久性不可逆的變化。

在blockchain_go中transaction的定義如下:

// TXInput represents a transaction inputtype TXInput struct {    Txid      []byte      Vout      int        Signature []byte    PubKey    []byte}// TXOutput represents a transaction outputtype TXOutput struct {    Value      int    PubKeyHash []byte}type Transaction struct {    ID   []byte        //交易唯一ID    Vin  []TXInput     //交易輸入序列    Vout []TXOutput    //交易輸出序列}

從定義可以看到Transaction就是輸入和輸出的集合,輸入和輸出的關係如下圖: 

其中tx0,tx1,tx2等是獨立的交易,每個交易通過輸入產生輸出,下麵重點看看一個交易的輸入和輸出單位是怎麼回事。

先看輸出TXOutput:

  • Value : 表示這個輸出中的代幣數量

  • PubKeyHash : 存放了一個用戶的公鑰的hash值,表示這個輸出裡面的Value是屬於哪個用戶的

輸入單元TXInput:

  • Txid : 交易ID(這個輸入使用的是哪個交易的輸出)

  • Vout : 該輸入單元指向本次交易輸出陣列的下標,通俗講就是,這個輸入使用的是Txid中的第幾個輸出。

  • Signature : 輸入發起方(轉賬出去方)的私鑰簽名本Transaction,表示自己認證了這個輸入TXInput。

  • PubKey : 輸入發起方的公鑰

通俗來講,一個TXInput結構表示 :

我要使用哪個交易(Txid)的哪個輸出陣列(Transaction.Vout)的下標(Vout)作為我本次輸入的代幣數值(TXOutput.Value)

因為交易的輸入其實是需要指明要輸入多少代幣(Value),但是TXInput中並沒有直接的代幣欄位,而唯一有代幣欄位的是在TXOuput中,所以這裡使用的方式是在TXInput中指明瞭自己需要使用的代幣在哪個TXOutput中。

TXInput中的Signature欄位是發起用戶對本次交易輸入的簽名,PubKey存放了用戶的公鑰,用於之前的驗證(私鑰簽名,公鑰驗證)。

3.什麼是UTXO

UTXO 是 Unspent Transaction Output 的縮寫,意指“為花費的交易輸出”,是中本聰最早在比特幣中採用的一種技術方案。因為比特幣中沒有賬戶的概念,也就沒有儲存用戶餘額數值的機制。因為區塊鏈中的歷史交易都是被儲存且不可修改的,而每一個交易(如前所述的Transaction)中又儲存了“誰轉移了多少給誰”的信息,所以要計算用戶賬戶餘額,只需要遍歷所有交易進行累計即可。

從第三節的交易圖可以看到,每筆交易的輸入TXInput都是使用的是其他交易的輸出TXOutput(只有輸出中儲存了該輸出是屬於哪個用戶,價值多少)。如果一筆交易的輸出被另外一個交易的輸入取用了(TXInput中的Vout指向了該TXOutput),那麼這筆輸出就是“已花費”。如果一筆交易的輸出沒有被任何交易的輸入取用,那麼就是“未花費”。分析上圖的tx3交易:

tx3有3個輸入:

  • input 0 :來自tx0的output0,花費了這個tx0.output0.

  • input 1 :來自tx1的output1,花費了這個tx1.output1.

  • input 2 :來自了tx2的output0,花費了這個tx2.output0.

tx3有2個輸出:

  • output 0 :沒有被任何後續交易取用,表示“未花費”。

  • output 1 :被tx4的input1取用,表示已經被花費。

因為每一個output都包括一個value和一個公鑰身份,所以遍歷所有區塊中的交易,找出其中所有“未花費”的輸出,就可以計算出用戶的賬戶餘額。

4.查找未花費的Output

如果一個賬戶需要進行一次交易,把自己的代幣轉給別人,由於沒有一個賬號系統可以直接查詢餘額和變更,而在utxo模型裡面一個用戶賬戶餘額就是這個用戶的所有utxo(未花費的輸出)記錄的合集,因此需要查詢用戶的轉賬額度是否足夠,以及本次轉賬需要消耗哪些output(將“未花費”的output變成”已花費“的output),通過遍歷區塊鏈中每個區塊中的每個交易中的output來得到結果。

下麵看看怎麼查找一個特定用戶的utxo,utxo_set.go相關代碼如下:

// FindSpendableOutputs finds and returns unspent outputs to reference in inputsfunc (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 {                    accumulated += out.Value                    unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)                }            }        }        return nil    })    if err != nil {        log.Panic(err)    }    return accumulated, unspentOutputs }

FindSpendableOutputs查找區塊鏈上pubkeyHash賬戶的utxo集合,直到這些集合的累計未花費金額達到需求的amount為止。

blockchain_go中使用嵌入式key-value資料庫boltdb儲存區塊鏈和未花費輸出等信息,其中utxoBucket是所有用戶未花費輸出的bucket,其中的key表示交易ID,value是這個交易中未被取用的所有output的集合。所以通過遍歷查詢本次交易需要花費的output,得到Transaction的txID和這個output在Transaction中的輸出陣列中的下標組合unspentOutputs。

另外一個重點是utxobucket中儲存的未花費輸出結合是關於所有賬戶的,要查詢特定賬戶需要對賬戶進行判斷,因為TXOutput中有pubkeyhash欄位,用來表示該輸出屬於哪個用戶,此處採用out.IsLockedWithKey(pubkeyHash)判斷特定output是否是屬於給定用戶。

5.新建Transaction

需要發起一筆交易的時候,需要新建一個Transaction,通過交易發起人的錢包得到足夠的未花費輸出,構建出交易的輸入和輸出,完成簽名即可,blockchain_go中的實現如下:

// NewUTXOTransaction creates a new transactionfunc 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; }

函式引數:

  • wallet : 用戶錢包引數,儲存用戶的公私鑰,用於交易的簽名和驗證。

  • to : 交易轉賬的目的地址(轉賬給誰)。

  • amount : 需要交易的代幣額度。

  • UTXOSet : uxto集合,查詢用戶的未花費輸出。

查詢需要的未花費輸出:

   acc, validOutputs := UTXOSet.FindSpendableOutputs(pubKeyHash, amount)

因為用戶的總金額是通過若干未花費輸出累計起來的,而每個output所攜帶金額不一而足,所以每次轉賬可能需要消耗多個不同的output,而且還可能涉及找零問題。以上查詢傳回了一批未花費輸出串列validOutputs和他們總共的金額acc. 找出來的未花費輸出串列就是本次交易的輸入,並將輸出結果構造output指向目的用戶,並檢查是否有找零,將找零返還。

如果交易順利完成,轉賬發起人的“未花費輸出”被消耗掉變成了花費狀態,而轉賬接收人to得到了一筆新的“未花費輸出”,之後他自己需要轉賬時,查詢自己的未花費輸出,即可使用這筆錢。

最後需要對交易進行簽名,表示交易確實是由發起人本人發起(私鑰簽名),而不是被第三人冒充。

6.Transaction的簽名和驗證

6.1 簽名

交易的有效性需要首先建立在發起人簽名的基礎上,防止他人冒充轉賬或者發起人抵賴,blockchain_go中交易簽名實現如下:

// SignTransaction signs inputs of a Transactionfunc (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) }// Sign signs each input of a Transactionfunc (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {    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        dataToSign := fmt.Sprintf("%x\n", txCopy)        r, s, err := ecdsa.Sign(rand.Reader, &privKey;, []byte(dataToSign))        if err != nil {            log.Panic(err)        }        signature := append(r.Bytes(), s.Bytes()...)        tx.Vin[inID].Signature = signature        txCopy.Vin[inID].PubKey = nil    } }

交易輸入的簽名信息是放在TXInput中的signature欄位,其中需要包括用戶的pubkey,用於之後的驗證。需要對每一個輸入做簽名。

6.2 驗證

交易簽名是發生在交易產生時,交易完成後,Transaction會把交易廣播給鄰居。節點在進行挖礦時,會整理一段時間的所有交易信息,將這些信息打包進入新的區塊,成功加入區塊鏈以後,這個交易就得到了最終的確認。但是在挖礦節點打包交易前,需要對交易的有效性做驗證,以防虛假資料,驗證實現如下:

// MineBlock mines a new block with the provided transactionsfunc (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")        }    }    ...    ...    ...    return block }// VerifyTransaction verifies transaction input signaturesfunc (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) }// Verify verifies signatures of Transaction inputsfunc (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):])        dataToVerify := fmt.Sprintf("%x\n", txCopy)        rawPubKey := ecdsa.PublicKey{Curve: curve, X: &x;, Y: &y;}        if ecdsa.Verify(&rawPubKey;, []byte(dataToVerify), &r;, &s;) == false {            return false        }        txCopy.Vin[inID].PubKey = nil    }    return true}

可以看到驗證的時候也是每個交易的每個TXInput都單獨進行驗證,和簽名過程很相似,需要構造相同的交易資料txCopy,驗證時會用到簽名設置的TxInput.PubKeyHash生成一個原始的PublicKey,將前面的signature分拆後通過ecdsa.Verify進行驗證。

7.總結

以上簡單分析和整理了blockchain_go中的交易和UTXO機制的實現過程,加深了區塊鏈中的挖礦,交易和轉賬的基礎技術原理的理解。

原文轉載自石匠的Blog

點擊“閱讀原文”

赞(0)

分享創造快樂