go公链实战0x07转账(2)

3 阅读8分钟

上节已基本实现硬编码转账并查询余额,今天真正地实现转账函数并对相关函数做一个优化。

UTXO

UTXO 代表 Unspent Transaction TxOutput,表示区块链上未经花费的交易输出。简单地说,UTXO还没有被包含在任何的交易输入中。根据UTXO可以知道对应TxOutput来自哪一笔交易,以及其在Vounts中的下标。

type UTXO struct {
	//来自交易的哈希
	TxHash []byte
	//在该交易VOuts里的下标
	Index int
	//未花费的交易输出
	Output *TXOutput
}

UTXOs函数改造

有了UTXO的结构后,我们就可以改造上次获取未花费输出的方法,使其返回为UTXO类型的数组。

其次,之前测试的都是单笔转账的交易。当出现多笔转账的交易时,我们现有的查询余额方法会不准确。为什么呢?

当一笔交易中有多个转账,当进行其中第二笔转账时,第一笔转账已经成功。但是,我们此时查询的依然是区块链上所有交易的UTXO。因此,我们还需要在UTXOs方法中加上当前未上链的所有交易的UTXO。

这时就有疑问了,不是只有上链的交易才会有效吗?事实是这样的,但是看目前的项目,由于还没有引入竞争挖矿的概念,每一次send必然会挖矿成功,其交易必然会上链。所以我们需要暂时这么做。

//5.返回一个地址对应的UTXO的交易UTXOs
//func (blc *Blockchain) UnSpentTransactionsWithAddress(address string) []*Transaction {
func (blc *Blockchain) UTXOs(address string, txs []*Transaction) []*UTXO {

	//未花费的TXOutput
	var utxos []*UTXO

	//已经花费的TXOutput [hash:[]] [交易哈希:TxOutput对应的index]
	var spentTXOutputs = make(map[string][]int)

	//遍历器处理区块链上的UTXO
	blcIterator := blc.Iterator()
	for {

		block := blcIterator.Next()

		//fmt.Println(block)
		//fmt.Println()

		for _, tx := range block.Txs {

			// txHash

			// Vins
			//判断当前交易是否为创币交易
			if tx.IsCoinbaseTransaction() == false {

				for _, in := range tx.Vins {

					//验证当前输入是否是当前地址的
					if in.UnlockWithAddress(address) {

						key := hex.EncodeToString(in.TxHash)

						//fmt.Printf("lll%x\n", in.TxHash)
						//fmt.Println(key)
						spentTXOutputs[key] = append(spentTXOutputs[key], in.Vout)
					}

				}
			}

			// Vouts
		Work:
			for index, out := range tx.Vouts {

				//验证当前输出是否是
				if out.UnLockScriptPubKeyWithAddress(address) {

					//fmt.Println(out)
					//fmt.Println(spentTXOutputs)

					//判断是否曾发生过交易
					if spentTXOutputs != nil {

						if len(spentTXOutputs) != 0 {

							//未花费UTXO标志
							isUnSpentUTXO := true

							//遍历spentTXOutputs
							for txHash, indexArray := range spentTXOutputs {

								//遍历TXOutputs下标数组
								for _, i := range indexArray {

									if index == i && txHash == hex.EncodeToString(tx.TxHAsh) {

										isUnSpentUTXO = false
										continue Work
									}
								}
							}

							if isUnSpentUTXO {

								utxo := &UTXO{tx.TxHAsh, index, out}
								utxos = append(utxos, utxo)
							}
						} else {

							utxo := &UTXO{tx.TxHAsh, index, out}
							utxos = append(utxos, utxo)
						}
					}
				}
			}
		}

		//找到创世区块,跳出循环
		var hashInt big.Int
		hashInt.SetBytes(block.PrevBlockHash)

		// Cmp compares x and y and returns:
		//
		//   -1 if x <  y
		//    0 if x == y
		//   +1 if x >  y
		if hashInt.Cmp(big.NewInt(0)) == 0 {

			break
		}
	}

	//处理未打包到区块链上的交易集里的UTXO
	for _, tx := range txs {

		if tx.IsCoinbaseTransaction() == false {
			for _, in := range tx.Vins {

				if in.UnlockWithAddress(address) {

					key := hex.EncodeToString(in.TxHash)

					spentTXOutputs[key] = append(spentTXOutputs[key], in.Vout)
				}
			}
		}
	}

	for _, tx := range txs {
	Work1:
		for index, out := range tx.Vouts {

			if out.UnLockScriptPubKeyWithAddress(address) {

				if len(spentTXOutputs) != 0 {

					for hash, indexArray := range spentTXOutputs {

						txHashStr := hex.EncodeToString(tx.TxHAsh)

						if hash == txHashStr {

							isUnSpentUTXO := true

							for _, outIndex := range indexArray {

								if index == outIndex {

									isUnSpentUTXO = false
									continue Work1
								}

								if isUnSpentUTXO {

									utxo := &UTXO{tx.TxHAsh, index, out}
									utxos = append(utxos, utxo)
								}
							}
						} else {

							utxo := &UTXO{tx.TxHAsh, index, out}
							utxos = append(utxos, utxo)
						}
					}
				} else {

					utxo := &UTXO{tx.TxHAsh, index, out}
					utxos = append(utxos, utxo)
				}
			}
		}
	}

	return utxos
}

TXInput和TXOutput解锁

上面UTXOs方法求得是某一个address的所有UTXO,目前我们还没有引入钱包地址的概念,姑且理解这个address为用户名。我们要想保证查询的是某个用户(address)交易输入和输出是属于这个用户的,必须有一个保障的机制。


//验证当前输入是否是当前地址的
func (txInput *TXInput) UnlockWithAddress(address string) bool  {

	return txInput.ScriptSig == address
}

//验证当前交易输出属于某用户
func (txOutput *TXOutput) UnLockScriptPubKeyWithAddress(address string) bool {

	return txOutput.ScriptPubKey == address
}

FindSpendableUTXOs

当我们进行一笔转账时,交易输入有可能引用一个UTXO,也可能引用多个UTXO。在获取转账方所有的UTXO后,还需要找到符合条件的UTXO组合作为交易输入的引用。这个时候可能出现用户余额不足以转账的情况,也可能出现UTXO组合价值大于转账金额产生找零的情况。

为了方便地判断UTXO来源以及计算转账后的找零,我们需要想办法在当前用户的所有UTXO中找到一个满足当前转账情况的UTXO集,并返回其UTXO总额和对应的UTXO集。而这个UTXO集是一个字典类型,键是UTXO来源交易的哈希,值对该交易下UTXO对应TXOutput在Vounts中的下标。

//转账时查找可用的用于消费的UTXO  返回输入总金额和一个字典,UTXO集是一个字典类型,键是UTXO来源交易的哈希,值对该交易下UTXO对应TXOutput在Vounts中的下标
func (blc *Blockchain) FindSpendableUTXOs(address string, amount int, txs []*Transaction) (int64, map[string][]int) {

	//1.获取当前地址所有UTXO
	utxos := blc.UTXOs(address, txs)
	spendableUTXO := make(map[string][]int)

	//2.遍历UTXO
	//总的金额
	var value int64
	for _, utxo := range utxos {

		value += utxo.Output.Value
		txHash := hex.EncodeToString(utxo.TxHash)
		spendableUTXO[txHash] = append(spendableUTXO[txHash], utxo.Index)

		if value >= int64(amount) {

			break
		}
	}

	//余额不足
	if value < int64(amount) {

		fmt.Println("%s found.余额不足...", value)
		os.Exit(1)
	}

	return value, spendableUTXO
}

NewTransaction

上次我们硬编码测试了几笔交易,这回有了上面的基础方法就可以对普通交易的构造做一个代码实现。

//2.普通交易
func NewTransaction(from string, to string, amount int, blc *Blockchain, txs []*Transaction) *Transaction {

	//获取from用户用于这笔交易的总输入金额和UTXO集
	money, spendableUTXODic := blc.FindSpendableUTXOs(from, amount, txs)

	//输入输出
	var txInputs []*TXInput
	var txOutputs []*TXOutput

	//遍历spendableUTXODic来组装TXInput作为该交易的交易输入
	for txHash, indexArr := range spendableUTXODic {

		//字符串转换为[]byte
		txHashBytes, _ := hex.DecodeString(txHash)
		for _, index := range indexArr {

			//交易输入
			txInput := &TXInput{
				txHashBytes,
				index,
				from,
			}
			txInputs = append(txInputs, txInput)
		}
	}

	//转账
	txOutput := &TXOutput{
		int64(amount),
		to,
	}
	txOutputs = append(txOutputs, txOutput)

	//找零
	txOutput = &TXOutput{
		money-int64(amount),
		from,
	}
	txOutputs = append(txOutputs, txOutput)

	//交易构造
	tx := &Transaction{
		[]byte{},
		txInputs,
		txOutputs,
	}

	tx.HashTransactions()

	return tx
}

MineNewBlock

理论上我们的交易是支持多笔转账的,可是上面构建交易的方法是针对一笔交易。所以,我们需要在发起交易挖掘区块的方法里对cli输入的多笔交易信息做一个遍历并生成多笔交易数据。

//2.新增一个区块到区块链 --> 包含交易的挖矿
func (blc *Blockchain) MineNewBlock(from []string, to []string, amount []string) {

	//send -from '["chaors"]' -to '["xyx"]' -amount '["5"]'

	//1.通过相关算法建立Transaction数组
	var txs []*Transaction

	//遍历输入输出,组装多笔交易
	for index, address := range from {

		value, _ := strconv.Atoi(amount[index])
		tx := NewTransaction(address, to[index], value, blc, txs)
		txs = append(txs, tx)
	}

	//2.挖矿
	//取上个区块的哈希和高度值
	var block *Block
	err := blc.DB.View(func(tx *bolt.Tx) error {

		b := tx.Bucket([]byte(blockTableName))
		if b != nil {

			hash := b.Get([]byte(newestBlockKey))
			blockBytes := b.Get(hash)
			block = DeSerializeBlock(blockBytes)
		}

		return nil
	})
	if err != nil {

		log.Panic(err)
	}

	//3.建立新区块
	block = NewBlock(txs, block.Height+1, block.Hash)

	//4.存储新区块
	err = blc.DB.Update(func(tx *bolt.Tx) error {

		b := tx.Bucket([]byte(blockTableName))
		if b != nil {

			//fmt.Printf("444---%x\n\n", block.Txs[0].Vins[0].TxHash)
			//fmt.Println(block)

			err = b.Put(block.Hash, block.Serialize())
			if err != nil {

				log.Panic(err)
			}

			err = b.Put([]byte(newestBlockKey), block.Hash)
			if err != nil {

				log.Panic(err)
			}

			blc.Tip = block.Hash
		}

		return nil
	})
	if err != nil {

		log.Panic(err)
		//fmt.Print(err)
	}
}

CLI优化

上面已经基本实现了多笔交易的打包并挖矿。接下来,我们看一下CLI.go文件的结构:

type CLI struct {

}

//打印目前左右命令使用方法
func printUsage() {
	fmt.Println("Usage:")
	fmt.Println("\tcreateBlockchain -address --创世区块地址 ")
	fmt.Println("\tsend -from FROM -to TO -amount AMOUNT --交易明细")
	fmt.Println("\tprintchain --打印所有区块信息")
	fmt.Println("\tgetbalance -address -- 输出区块信息.")
}

func isValidArgs() {

	//获取当前输入参数个数
	if len(os.Args) < 2 {
		printUsage()
		os.Exit(1)
	}
}

func (cli *CLI) Run() {

	isValidArgs()

	//自定义cli命令
	sendBlockCmd := flag.NewFlagSet("send", flag.ExitOnError)
	printchainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
	createBlockchainCmd := flag.NewFlagSet("createBlockchain", flag.ExitOnError)
	blanceBlockCmd := flag.NewFlagSet("getBalance", flag.ExitOnError)

	//addBlockCmd 设置默认参数
	flagSendBlockFrom := sendBlockCmd.String("from", "", "源地址")
	flagSendBlockTo := sendBlockCmd.String("to", "", "目标地址")
	flagSendBlockAmount := sendBlockCmd.String("amount", "", "转账金额")
	flagCreateBlockchainAddress := createBlockchainCmd.String("address", "", "创世区块地址")
	flagBlanceBlockAddress := blanceBlockCmd.String("address", "", "输出区块信息")

	//解析输入的第二个参数是addBlock还是printchain,第一个参数为./main
	switch os.Args[1] {
	case "send":
		//第二个参数为相应命令,取第三个参数开始作为参数并解析
		err := sendBlockCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	case "printchain":
		err := printchainCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	case "createBlockchain":
		err := createBlockchainCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	case "getBalance":
		err := blanceBlockCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	default:
		printUsage()
		os.Exit(1)
	}

	//对addBlockCmd命令的解析
	if sendBlockCmd.Parsed() {

		if *flagSendBlockFrom == "" {

			printUsage()
			os.Exit(1)
		}
		if *flagSendBlockTo == "" {

			printUsage()
			os.Exit(1)
		}
		if *flagSendBlockAmount == "" {

			printUsage()
			os.Exit(1)
		}

		//cli.addBlock(*flagAddBlockData)

		//这里真正地调用转账方法
		//fmt.Println(*flagSendBlockFrom)
		//fmt.Println(*flagSendBlockTo)
		//fmt.Println(*flagSendBlockAmount)
		//
		//fmt.Println(Json2Array(*flagSendBlockFrom))
		//fmt.Println(Json2Array(*flagSendBlockTo))
		//fmt.Println(Json2Array(*flagSendBlockAmount))
		cli.send(
			Json2Array(*flagSendBlockFrom),
			Json2Array(*flagSendBlockTo),
			Json2Array(*flagSendBlockAmount),
			)
	}
	//对printchainCmd命令的解析
	if printchainCmd.Parsed() {

		cli.printchain()
	}
	//
	if createBlockchainCmd.Parsed() {

		if *flagCreateBlockchainAddress == "" {

			cli.creatBlockchain(*flagCreateBlockchainAddress)
		}

		cli.creatBlockchain(*flagCreateBlockchainAddress)
	}

	if blanceBlockCmd.Parsed() {

		if *flagBlanceBlockAddress == "" {

			printUsage()
			os.Exit(1)
		}

		cli.getBlance(*flagBlanceBlockAddress)
	}
}

不难返现逻辑不是很清晰,既有cli命令的定义和解析,又有具体命令的实现。按照单一职责的设计原则,这里应该只有cli命令的定义和解析,具体命令的解析应该拆分到相应文件。这样显得脉络清晰,逻辑明了。

例如,我们可以吧创建区块链命令的具体实现分离到一个CLI_createBlockchain.go文件:

//新建区块链
func (cli *CLI)creatBlockchain(address string)  {

	blockchain := CreateBlockchainWithGensisBlock(address)
	defer blockchain.DB.Close()
}

目前CLI支持的命令还有,打印区块链(printchain),获取余额(getBlance),转账(send),我们按照上面的处理方式分别把代码分离就可以了。最终,项目会多出这几个文件:

CLI优化.png

至此,就基本实现了公链的转账功能。

源代码在这,喜欢的朋友记得给个小star,或者fork.也欢迎大家一起探讨区块链相关知识,一起进步!

更多原创区块链技术文章请访问chaors

. . . .

###互联网颠覆世界,区块链颠覆互联网!

---------------------------------------------20180705 22:24