小白学习区块链

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

2018-07-05  本文已影响7人  WallisW

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

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.也欢迎大家一起探讨区块链相关知识,一起进步!

.
.
.
.

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

---------------------------------------------20180705 22:24
上一篇下一篇

猜你喜欢

热点阅读