通过blockchain_go分析区块链交易原理

Posted GoCN

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过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 : 用户钱包参数,存储用户的公私钥,用于交易的签名和验证。

  • 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

点击"阅读原文"

以上是关于通过blockchain_go分析区块链交易原理的主要内容,如果未能解决你的问题,请参考以下文章

区块链数据分析,让你看清交易对手

区块链教程——P2P交易原理

区块链教程之Bitcoin原理简介

比特币创建交易源码分析

区块链原理(结合实际例子较详细的讲述原理)

区块链原理(结合实际例子较详细的讲述原理)