go公链实战0x09之交易(2)

作者: WallisW | 来源:发表于2018-07-08 15:36 被阅读36次

前面最开始构建了交易的基本模型,然后逐步实现了转账,集成了钱包地址。公链基本交易模块已然成型,还有些小的细节需要去实现和优化。

Coinbase奖励

我们知道,在比特币中每当矿工成功挖到一个区块,就会得到一笔奖励。这笔奖励包含在创币交易中,创币交易是一个区块中的第一笔交易。

目前的项目还没有引入多节点竞争挖矿,暂且认为每一个区块是转账的第一个发起人挖到的。如此,Coinbase奖励就应该添加到MineNewBlock方法里。

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

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

    //获取UTXO集
    utxoSet := &UTXOSet{blc}

    var txs []*Transaction

    //作为奖励给矿工的奖励  暂时将这笔奖励给from[0]  挖矿成功后再转给挖矿的矿工
    tx := NewCoinbaseTransaction(from[0])
    txs = append(txs, tx)

    //1.通过相关算法建立Transaction数组
    for index, address := range from {

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

UTXOSet

之前为了实现转账引入了UTXO的概念,但是我们每次在转账查询可用余额时,都会去遍历一遍数据库上的区块。这样,会随着区块链的不断扩张,转账时查询的成本越来越高。

毕竟,查询时我们只需要关注未花费的TxOutput信息,而不需要关注区块上其他信息。那么,我们为什么不把未花费的TxOutput信息单独存储来查询呢?

其实,比特币正是这样做的。Bitcoin将链上所有区块存储在blocks数据库,将所有UTXO的集存储在chainstate数据库。

于是,我们引入UTXOSet集用来实现UTXO集的数据库存储。

//存储未花费交易输出的数据库表
const UTXOTableName  = "UTXOTableName"

type UTXOSet struct {

    Blockchain *Blockchain
}

ResetUTXOSet

该方法初始化UTXO集的存储表,如果bucket 存在就先移除,然后从区块链中获取所有的未花费输出,最终将输出保存到 bucket 中。

// 重置数据库表
func (utxoSet *UTXOSet) ResetUTXOSet()  {

    err := utxoSet.Blockchain.DB.Update(func(tx *bolt.Tx) error {

        b := tx.Bucket([]byte(utxoTableName))

        if b != nil {


            err := tx.DeleteBucket([]byte(utxoTableName))

            if err!= nil {
                log.Panic(err)
            }

        }

        b ,_ = tx.CreateBucket([]byte(utxoTableName))
        if b != nil {

            //[string]*TXOutputs
            txOutputsMap := utxoSet.Blockchain.FindUTXOMap()


            for keyHash,outs := range txOutputsMap {

                txHash,_ := hex.DecodeString(keyHash)

                b.Put(txHash,outs.Serialize())

            }
        }

        return nil
    })

    if err != nil {
        log.Panic(err)
    }
}

既然是UTXO存储表的初始化,那么一般情况下它只被执行一次。这种特性和创世区块相似,所以我们需要把他的实现放到创世区块的创建中。

//1.创建创世区块
func CreateBlockchainWithGensisBlock(address string) *Blockchain {

    ...
    ...
    
    //创建创世区块时候初始化UTXO表
    utxoSet := &UTXOSet{blc}
    utxoSet.ResetUTXOSet()

    return blc
}

UTXO表存储格式

我们存储UTXO的目的也是为了转账时能够更快地查询余额,为了实现转账,UTXO表除了存储对应的未花费的TXOutput集,还需要存储这些TXOutput来自于哪一笔交易。

我们以交易的哈希为键,以该交易下TXOutput组成的数组为值来存储UTXO。

由于一个交易下可能存在多个TXOutput,显然可以用数组表示。我们引入TXOutputs类来表示,因为要存储这些TXOutput需要实现序列化。

TXOutputs

type TXOutputs struct {

    UTXOS []*UTXO
}


// 序列化成字节数组
func (txOutputs *TXOutputs) Serialize() []byte {

    var result bytes.Buffer

    encoder := gob.NewEncoder(&result)

    err := encoder.Encode(txOutputs)
    if err != nil {
        log.Panic(err)
    }

    return result.Bytes()
}

// 反序列化
func DeserializeTXOutputs(txOutputsBytes []byte) *TXOutputs {

    var txOutputs TXOutputs

    decoder := gob.NewDecoder(bytes.NewReader(txOutputsBytes))
    err := decoder.Decode(&txOutputs)
    if err != nil {

        log.Panic(err)
    }

    return &txOutputs
}

FindUTXOMap

知道UTXO表的存储格式后,我们就需要能够找到区块链上所有的未花费的TXOutput,并且能够以UTXO表存储的格式返回。这个方法是基于Blockchain的。

// 查找未花费的UTXO[string]*TXOutputs 返回字典  键为所属交易的哈希,值为TXOutput数组
func (blc *Blockchain) FindUTXOMap() map[string]*TXOutputs  {

    blcIterator := blc.Iterator()

    // 存储已花费的UTXO的信息
    spentableUTXOsMap := make(map[string][]*TXInput)

    utxoMaps := make(map[string]*TXOutputs)


    for {

        block := blcIterator.Next()

        for i := len(block.Txs) - 1; i >= 0 ;i-- {

            txOutputs := &TXOutputs{[]*UTXO{}}
            tx := block.Txs[i]

            // coinbase
            if tx.IsCoinbaseTransaction() == false {

                for _,txInput := range tx.Vins {

                    txHash := hex.EncodeToString(txInput.TxHash)
                    spentableUTXOsMap[txHash] = append(spentableUTXOsMap[txHash],txInput)
                }
            }

            txHash := hex.EncodeToString(tx.TxHAsh)

        WorkOutLoop:
            for index,out := range tx.Vouts  {

                txInputs := spentableUTXOsMap[txHash]

                if len(txInputs) > 0 {

                    isUnSpent := true

                    for _,in := range  txInputs {

                        outPublicKey := out.Ripemd160Hash
                        inPublicKey := in.PublicKey

                        if bytes.Compare(outPublicKey,Ripemd160Hash(inPublicKey)) == 0{

                            if index == in.Vout {

                                isUnSpent = false
                                continue WorkOutLoop
                            }
                        }

                    }

                    if isUnSpent {

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

                } else {

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

            }

            // 设置键值对
            utxoMaps[txHash] = txOutputs
        }


        // 找到创世区块时退出
        var hashInt big.Int
        hashInt.SetBytes(block.PrevBlockHash)
        if hashInt.Cmp(big.NewInt(0)) == 0 {

            break
        }
    }

    return utxoMaps
}

UTXOSet测试

我们在命令行新增一个命令用于打印目前UTXO表存储的内容。

test命令添加和解析

testCmd := flag.NewFlagSet("test", flag.ExitOnError)

...
...

switch os.Args[1] {
     ...
     ...
    case "test":
        //第二个参数为相应命令,取第三个参数开始作为参数并解析
        err := testCmd.Parse(os.Args[2:])
        if err != nil {
            log.Panic(err)
        }
}

...
...

//UTXOSet测试
if testCmd.Parsed() {

    cli.TestMethod()
}

CLI_test.go

func (cli *CLI) TestMethod()  {


    fmt.Println("TestMethod")

    blockchain := GetBlockchain()
    defer blockchain.DB.Close()

    utxoSet := &UTXOSet{blockchain}
    utxoSet.ResetUTXOSet()

    fmt.Println(blockchain.FindUTXOMap())
}

GetBalance

现在我们查询余额就不需要去遍历整个区块数据库了,只需要遍历存储UTXO的表即可。查询逻辑还是和之前一样,先要从表中查到对应地址的所有UTXO,然后累加他们的值。

// 3.查询余额
func (utxoSet *UTXOSet) GetBalance(address string) int64 {

    UTXOS := utxoSet.FindUTXOsForAddress(address)

    var amount int64

    for _, utxo := range UTXOS  {

        amount += utxo.Output.Value
    }

    return amount
}

// 2.查询某个地址的UTXO
func (utxoSet *UTXOSet) FindUTXOsForAddress(address string) []*UTXO {

    var utxos []*UTXO

    err := utxoSet.Blockchain.DB.View(func(tx *bolt.Tx) error {

        b := tx.Bucket([]byte(UTXOTableName))

        // 游标
        c := b.Cursor()
        for k, v := c.First(); k != nil; k,v = c.Next() {

            txOutputs := DeserializeTXOutputs(v)

            for _, utxo := range txOutputs.UTXOS {

                if utxo.Output.UnLockScriptPubKeyWithAddress(address) {

                    utxos = append(utxos,utxo)
                }
            }
        }

        return nil
    })
    if err != nil {

        log.Panic(err)
    }

    return utxos
}

接下来,修改CLI中查询余额调用的方法即可。


//查询余额
func (cli *CLI) getBlance(address string) {

    fmt.Println("地址:" + address)

    blockchain := GetBlockchain()
    defer blockchain.DB.Close()

    //amount := blockchain.GetBalance(address)  引入UTXOSet前的方法

    utxoSet := &UTXOSet{blockchain}
    amount := utxoSet.GetBalance(address)

    fmt.Printf("%s一共有%d个Token\n", address, amount)
}

send转账优化

我们来回忆一下转账的几个重要步骤:

1.找到发起方所有的UTXO
2.在1的UTXO集里找到符合该次转账条件的UTXO组合和该组合对应的代币总和。
3.发起转账

之前上面的1,2都是在Blockchain里实现,并需要遍历整个区块数据库。在引入UTXOSet之后就需要基于UTXOSet实现。

1.Blockchain.UTXOs --> UTXOSet.FindUnPackageSpendableUTXOS

2.Blockchain.FindSpendableUTXOs --> UTXOSet.FindSpendableUTXOs

显然,2的方法基本相同。但是1有所差别,因为之前的逻辑中Blockchain.UTXOs需要找到链上所有UTXO和未打包的UTXO,而引入UTXOSet之后链上的UTXO都在UTXO表中,我们只需要找到未打包的交易产生的UTXO即可。

找到未打包交易的UTXO

// 找到未打包交易的UTXO,对应TXOutput的TX的Hash和index
func (utxoSet *UTXOSet) FindUnPackageSpendableUTXOS(address string, txs []*Transaction) []*UTXO {

    var unUTXOs []*UTXO
    spentTXOutputs := make(map[string][]int)

    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 {

    Work:
        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 {

                            var isUnSpent =true
                            for _,outIndex := range indexArray {

                                if index == outIndex {

                                    isUnSpent = false
                                    continue Work
                                }

                                if isUnSpent {

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

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

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

    return unUTXOs
}

找到未花费交易里满足当次交易的UTXO组合

//转账时查找可用的用于消费的UTXO组合
func (utxoSet *UTXOSet) FindSpendableUTXOs(address string,amount int64,txs []*Transaction) (int64,map[string][]int)  {

    unPackageUTXOS := utxoSet.FindUnPackageSpendableUTXOS(address, txs)

    spentableUTXO := make(map[string][]int)

    var value int64 = 0

    for _, UTXO := range unPackageUTXOS {

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

        if value >= amount{

            return  value, spentableUTXO
        }
    }

    // 钱还不够
    err := utxoSet.Blockchain.DB.View(func(tx *bolt.Tx) error {

        b := tx.Bucket([]byte(UTXOTableName))

        if b != nil {

            c := b.Cursor()
        UTXOBREAK:
            for k, v := c.First(); k != nil; k, v = c.Next() {

                txOutputs := DeserializeTXOutputs(v)

                for _, utxo := range txOutputs.UTXOS {

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

                    if value >= amount {

                        break UTXOBREAK
                    }
                }
            }
        }

        return nil
    })
    if err != nil {

        log.Panic(err)
    }

    if value < amount{

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

    return  value, spentableUTXO
}

UTXOSet.Update

由于转账消耗了一定的UTXO,同时产生了一定的UTXO。所以转账之后需要对UTXO数据库表做更新以保持UTXO表存储的永远是最新的未花费的交易输出。

简单地梳理一下更新UTXO表的步骤:

1.找到最新添加到区块链上的区块

2.遍历区块交易,将所有交易输入集中到一个数组

3.遍历区块交易的交易输出,找到新增的未花费的TXOutput

4.在UTXO表中删除输入输入中已花费的TXOutput,并将未花费的TXOutput缓存

5.将3求出的和4缓存的TXOutput新增到UTXO表中

废话少说撸代码

//更新UTXO 
func (utxoSet *UTXOSet) Update()  {

    // 1.找出最新区块
    block := utxoSet.Blockchain.Iterator().Next()

    // 未花费的UTXO  键为对应交易哈希,值为TXOutput数组
    outsMap := make(map[string] *TXOutputs)
    // 新区快的交易输入,这些交易输入引用的TXOutput被消耗,应该从UTXOSet删除
    ins := []*TXInput{}

    // 2.遍历区块交易找出交易输入
    for _, tx := range block.Txs {

        //遍历交易输入,
        for _, in := range tx.Vins {

            ins = append(ins, in)
        }
    }

    // 2.遍历交易输出
    for _, tx := range block.Txs {

        utxos := []*UTXO{}

        for index, out := range tx.Vouts {

            //未花费标志
            isUnSpent := true
            for _, in := range ins {

                if in.Vout == index && bytes.Compare(tx.TxHAsh, in.TxHash) == 0 &&
                    bytes.Compare(out.Ripemd160Hash, Ripemd160Hash(in.PublicKey)) == 0 {

                        isUnSpent = false
                        continue
                }
            }

            if isUnSpent {

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

        if len(utxos) > 0 {

            txHash := hex.EncodeToString(tx.TxHAsh)
            outsMap[txHash] = &TXOutputs{utxos}
        }
    }

    //3. 删除已消耗的TXOutput
    err := utxoSet.Blockchain.DB.Update(func(tx *bolt.Tx) error {

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

            for _, in := range ins {

                txOutputsBytes := b.Get(in.TxHash)

                //如果该交易输入无引用的交易哈希
                if len(txOutputsBytes) == 0 {

                    continue
                }
                txOutputs := DeserializeTXOutputs(txOutputsBytes)

                // 判断是否需要
                isNeedDelete := false

                //缓存来自该交易还未花费的UTXO
                utxos := []*UTXO{}

                for _, utxo := range txOutputs.UTXOS {

                    if in.Vout == utxo.Index && bytes.Compare(utxo.Output.Ripemd160Hash, Ripemd160Hash(in.PublicKey)) == 0 {

                        isNeedDelete = true
                    }else {

                        //txOutputs中剩余未花费的txOutput
                        utxos = append(utxos,utxo)
                    }
                }

                if isNeedDelete {

                    b.Delete(in.TxHash)

                    if len(utxos) > 0 {

                        preTXOutputs := outsMap[hex.EncodeToString(in.TxHash)]
                        preTXOutputs.UTXOS = append(preTXOutputs.UTXOS, utxos...)
                        outsMap[hex.EncodeToString(in.TxHash)] = preTXOutputs
                    }
                }
            }

            // 4.新增交易输出到UTXOSet
            for keyHash, outPuts := range outsMap {

                keyHashBytes, _ := hex.DecodeString(keyHash)
                b.Put(keyHashBytes, outPuts.Serialize())
            }
        }

        return nil
    })
    if err != nil{

        log.Panic(err)
    }
}

Main_Test

package main

import (

    "chaors.com/LearnGo/publicChaorsChain/part11-transaction_1_Prototype/BLC"
)

func main() {

    cli := BLC.CLI{}
    cli.Run()
}

创建区块链

main_test1.png

产生交易后

main_test2.png

到目前为止,公链的交易算是基本讲完了。

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

.
.
.
.

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

---------------------------------------------20180708 15:33

相关文章

网友评论

    本文标题:go公链实战0x09之交易(2)

    本文链接:https://www.haomeiwen.com/subject/pwhouftx.html