1. 程式人生 > >區塊鏈比特幣交易原理

區塊鏈比特幣交易原理

1.比特幣背景

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

2.比特幣身份認證

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

// Wallet stores private and public keys
type Wallet struct {
    PrivateKey ecdsa.PrivateKey
    PublicKey  []byte
}

// NewWallet creates and returns a Wallet
func 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 input
type TXInput struct {
    Txid      []byte  
    Vout      int     
    Signature []byte
    PubKey    []byte
}
// TXOutput represents a transaction output
type 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 inputs
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 {
                    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 transaction
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
}

函式引數:

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 Transaction
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)
}

// Sign signs each input of a Transaction
func (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 transactions
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")
        }
    }

    ...
    ...
    ...

    return block
}
// VerifyTransaction verifies transaction input signatures
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)
}
// Verify verifies signatures of Transaction inputs
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):])

        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進行驗證。