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

原文链接-石匠的Blog

1.背景

在去中心化的区块链中进行交易(转账)是怎么实现的呢?本篇通过blockchain_go来分析一下。需要进行交易,首先就需要有交易的双方以及他们的认证机制,其次是各自的资金账户规则。在分布式账本系统里面,需要有机制能够准确验证一个用户身份以及对账户资金的精确计算,不能出现一丁点差错。在区块链中交易通过Transaction表示,而账户的自己并不是在每个节点上保存每个用户的一个余额的数字,而是通过历史交易信息计算而来(历史交易不可篡改),其中的关键机制是UTXO。

2.身份认证

在区块链身份认证是采用RSA非对称加密体系完成,每个用户在会拥有一个“钱包”,钱包是通过安全的椭圆曲线加密算法生成,其中包括一对公私钥。私钥自己保留不能暴露,用作加密,签名等,公钥公开给所有人,用于信息验证等。只要是用私钥签名的信息,就可以通过配对的公钥解码认证,不可抵赖。在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进行验证。

7.总结

以上简单分析和整理了blockchain_go中的交易和UTXO机制的实现过程,加深了区块链中的挖矿,交易和转账的基础技术原理的理解。

0 个评论

要回复文章请先登录注册