• 通过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
    ", 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
    ", 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机制的实现过程,加深了区块链中的挖矿,交易和转账的基础技术原理的理解。

  • 相关阅读:
    nginx和phpfpm保持长连接
    单件模式+打开窗体+窗体构造函数参数
    Java REST框架一览(转)
    什么原因成就了一位优秀的程序员?(转)
    使用 sqlRest 将数据库转换为 REST 风格的 Web 服务(转)
    浏览器插件之ActiveX开发系列(转载)
    Java JSON技术框架选型与实例(转)
    SQL参数绑定
    ab压力测试工具使用
    Jintegra使用注意事项
  • 原文地址:https://www.cnblogs.com/bugmaking/p/9313458.html
Copyright © 2020-2023  润新知