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

  • 相关阅读:
    进程池,线程池,协程,gevent模块,协程实现单线程服务端与多线程客户端通信,IO模型
    线程相关 GIL queue event 死锁与递归锁 信号量l
    生产者消费者模型 线程相关
    进程的开启方式 进程的join方法 进程间的内存隔离 其他相关方法 守护进程 互斥锁
    udp协议 及相关 利用tcp上传文件 socketserver服务
    socket套接字 tcp协议下的粘包处理
    常用模块的完善 random shutil shevle 三流 logging
    day 29 元类
    Django入门
    MySQL多表查询
  • 原文地址:https://www.cnblogs.com/bugmaking/p/9313458.html
Copyright © 2011-2022 走看看