go区块链公链实战0x06转账(1)

上次基本实现了交易的数据结构,在此基础上便可以来实现转账,即区块链的普通交易.

cli转账命令

我们知道挖矿的目的是找到一个公认的记账人把当前的所有交易打包到区块并添加到区块链上.之前我们使用addBlock命令实现添加区块到区块链的,这里转账包含挖矿并添加到区块链.所以,我们需要在cli工具类里用转账命令send代替addBlock命令.

其次我们都知道,一次区块可以包括多个交易.因此,这里我们的转账命令要设计成支持多笔转账.

//命令说明方法 打印目前左右命令使用方法
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 (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)
    }
}

Json2Arr

命令行输入的都是字符串,要想让转账命令支持多笔转账,则输入的信息是json形式的数组.在编码实现解析并转账的时候,我们需要将Json字符串转化为数组类型.这个功能在utils里实现.

我们一般输入的转账命令是这样的:

send -from '["chaors", "ww"]' -to '["xyz", "dh"]' -amount '["5", "100"]'

send 转账命令
from 发送方
to 接收方
amount 转账金额
三个参数的数组分别一一对应,上述命令表示:
chaors转给xyx共5btc;
ww转给dh的100btc.

utils.go

// 标准的JSON字符串转数组
func Json2Array(jsonString string) []string {

    //json 到 []string
    var sArr []string
    if err := json.Unmarshal([]byte(jsonString), &sArr); err != nil {

        log.Panic(err)
    }
    return sArr
}

转账的理解

说到转账,就离不开交易.这里的转账便是普通交易,之前我们只实现了创币交易.这里需要实现普通交易.

为了更好地理解转账的过程,我们先将复杂问题简单化.假设每一个区块只有一笔交易,我们看一个简单的小🌰.

1.节点chaors挖到一个区块,产生25BTC的创币交易。由于是创币交易,其本身是不需要引用任何交易输出的,所以在输入对象TXInput的交易哈希为空,vount所在的下标为-1,数字签名为空或者随便填写;输出对象里btc拥有者为chaors,面值为25btc 创世区块交易结构

 txInput0 = &TXInput{[]byte{},-1,"Gensis Block"}
 txOutput0 = &TXOutput{25, "chaors"}  //在gaVouts索引为0

 CoinbaseTransaction{"00000",
            []*TXInput{txInput0},
            []*TXOutput{txOutput0}
}

2.chaors获得25btc后,他的好友ww知道后向他索要10btc.大方的chaors便把10btc转给ww.此时
交易的输入为chaors上笔交易获得的btc,TXInput对象的交易ID为奖励chaors的上一个交易ID,vount下标为chaors的TXOutput下标,签名此时且认为是来自chaors,填作"chaors" 此时chaors的25btc面值的TXOutput就被花费不复存在了,那么chaors还应该有15btc的找零哪去了?系统会为chaors的找零新生成一个面值15btc的TXOutput。所以,这次有一个输入,两个输出。

chaors(25) 给 ww 转 10 -- >> chaors(15) + ww(10)

这次的交易结构为:

 //输入
 txInput1 = &TXInput{"00000",0,"chaors"}
 //"00000" 相当于来自于哈希为"00000"的交易
 //索引为零,相当于上一次的txOutput0为输入

 //输出
 txOutput1 = &TXOutput{10, "ww"}        //在该笔交易Vouts索引为0  chaors转给ww的10btc产生的输出
 txOutput2 = &TXOutput{15, "chaors"}    //在该笔交易Vouts索引为1  给ww转账产生的找零
 Transaction1{"11111",
            []*TXInput{txInput1}
            []*TXOutput{txOutput1, txOutput2}
}

3.ww感觉拥有比特币是一件很酷的事情,又来跟chaors要。出于兄弟情谊,chaors又转给ww7btc
这次的交易结构为:

//输入
 txInput2 = &TXInput{"11111",2,"chaors"}

 //输出
 txOutput3 = &TXOutput{7, "ww"}       //在该笔交易Vouts索引为0
 txOutput4 = &TXOutput{8, "chaors"}   //在该笔交易Vouts索引为1
 Transaction2{"22222",
            []*TXInput{txInput2}
            []*TXOutput{txOutput3, txOutput4}
}

4.消息传到他们共同的朋友xyz那里,xyz觉得btc很好玩向ww索要15btc.ww一向害怕xyx,于是尽管不愿意也只能屈服。

我们来看看ww此时的所有财产:

txOutput1 = &TXOutput{10, "ww"}     //来自Transaction1(hash:11111)Vouts索引为0的输出   
txOutput3 = &TXOutput{7, "ww"}      //来自Transaction2(hash:2222)Vouts索引为0的输出

想要转账15btc,ww的哪一笔txOutput都不够,这个时候就需要用ww的两个txOutput都作为
输入,这次的交易结构为:

//输入:
txInput3 = &TXInput{"11111",1,"ww"}
txInput4 = &TXInput{"22222",3,"ww"}

//输出
 txOutput5 = &TXOutput{15, "xyz"}       索引为5
 txOutput6 = &TXOutput{2, "ww"}        索引为6

 第四个区块交易结构
 Transaction3{"33333",
            []*TXInput{txInput3, txInput4}
            []*TXOutput{txOutput5, txOutput6}
}

现在,我们来总结一下上述几个交易.

A.chaors

1.从CoinbaseTransaction获得TXOutput0总额25
2.Transaction1转给ww10btc,TXOutput0被消耗,获得txOutput2找零15btc
3.Transaction2转给ww7Btc,txOutput2被消耗,获得txOutput4找零8btc
4.最后只剩8btc的txOutput4作为未花费输出

B.ww

1.从Transaction1获得TXOutput1,总额10btc
2.从Transaction2获得TXOutput3,总额7btc
3.Transaction3转给xyz15btc,TXOutput1和TXOutput3都被消耗,获得txOutput6找零2btc
4.最后只剩2btc的txOutput6作为未花费输出

C.xyz

1.从Transaction3获得TXOutput5,总额15btc
2.拥有15btc的TXOutput5作为未花费输出

经过这个例子,我们可以发现转账具备几个特点:

1.每笔转账必须有输入TXInput和输出TXOutput
2.每笔输入必须有源可查(TXInput.TxHash)
3.每笔输入的输出引用必须是未花费的(没有被之前的交易输入所引用)
4.TXOutput是一个不可分割的整体,一旦被消耗就不可用.消费额度不对等时会有找零(产生新的TXOutput)

这个🌰很重要,对于后面转账的代码逻辑是个扎实的基础准备.

AddBlockToBlockchain --> MineNewBlock

既然在cli工具用转账命令send代替了添加区块,那么在实际的函数调用中,我们必须考虑到交易信息.上面对转账有了一定的理解,现在可以认为构造公链的第一笔交易.

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


    //单笔交易构造假数据测试交易

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

    //输入,由于这里引用的TXOutput来自创世区块的奖励, 这里复制创世区块里创币交易的哈希作为交易输入对TXOutput的引用
    txHash, _ := hex.DecodeString("d3c17e00ad2c1bd7fec8f5afde710f2c3afd40478c3cca492d7e9a2b0cbe4808")
    txInput := &TXInput {
        txHash,
        0,  //要花费的TXOutput在对应交易的Vounts下标为0
        from[0],
    }

    fmt.Printf("111--%x\n", txInput.TxHash)

    txInputs = append(txInputs, txInput)

    //转账
    txOutput := &TXOutput{
        10,
    to[0],
    }
    txOutputs = append(txOutputs, txOutput)

    //找零
    txOutput = &TXOutput{
        25-10,
        from[0],
    }
    txOutputs = append(txOutputs, txOutput)

    tx := &Transaction{
        []byte{},
        txInputs,
        txOutputs,
    }

    tx.HashTransactions()

    fmt.Printf("222---%x\n", txInput.TxHash)

    return tx


    //1. 有一个函数,返回from这个人所有的未花费交易输出所对应的Transaction
    //unSpentTx := UnSpentTransactionsWithAddress("chaors")
    //fmt.Println(unSpentTx)
}

我们在人为通过硬编码构造好一个基于创世区块的转账交易后,此时需要将这笔交易打包到区块并添加到区块链上.之前我们的AddBlockToBlockchain就需要做些改动.

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

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

    tx := NewTransaction(from, to, amount)
    //1.通过相关算法建立Transaction数组
    var txs []*Transaction
    txs = append(txs, tx)

    fmt.Printf("333---%x\n\n", txs[0].Vins[0].TxHash)

    //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))
            block = DeSerializeBlock(b.Get(hash))
        }

        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工具将send命令的具体实现添加好.

//转账
func (cli *CLI) send(from []string, to []string, amount []string)  {

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

    blockchain.MineNewBlock(from, to, amount)
}

余额查询GetBalance

转账和区块上链都实现了,Run也是没有问题的.那么怎么验证转账成功呢?毕竟此时我们不知道各自的余额是多少.这时候,我们就需要来实现余额查询方法.

首先在CLi工具里实现,getBlance命令的添加和解析其实在前面说到send命令时已经有了.回去看看即可.

余额查询的实现

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

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

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

    amount := blockchain.GetBalance(address)

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

//查询余额
func (blc *Blockchain) GetBalance(address string) int64 {

    utxos := blc.UTXOs(address)

    var amount int64
    for _, out := range utxos {

        amount += out.Value
    }

    return amount
}

UTXOs

要想实现余额查询,必须知道某个账户未花费的TxOutput.这个时候我们需要遍历区块链上的区块,然后去每一笔交易里找.在每笔交易输出里被引用的TxOutput必定被消耗,只需要记录被消耗的TxOutput.然后再去比对每笔交易产生的TxOutput,做个去除即可得到当前账户在链上剩余的未花费的TxOutput.

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

    //未花费的TXOutput
    var UTXOs []*TXOutput

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

    //遍历器
    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
            for index, out := range tx.Vouts {

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

                    fmt.Printf("%x", block.Hash)
                    fmt.Println(index, out)

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

                        if len(spentTXOutputs) != 0 {

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

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

                                    fmt.Printf("%d--%d\n", index, i)
                                    fmt.Printf("%s\n", txHash)
                                    fmt.Printf("%x\n", tx.TxHAsh)
                                    fmt.Println(spentTXOutputs)
                                    fmt.Println(out)

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

                                        continue
                                    } else {

                                        //fmt.Println(index,i)
                                        //fmt.Println(out)
                                        //fmt.Println(spentTXOutputs)

                                        UTXOs = append(UTXOs, out)
                                    }
                                }
                            }
                        } else {

                            UTXOs = append(UTXOs, out)
                        }
                    }
                }
            }
        }

        //找到创世区块,跳出循环
        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
        }
    }

    return UTXOs
}

Main_test

package main

import (

    "chaors.com/publicChaorsChain/part8-transfer-Prototype/BLC"
)

func main() {

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

    //blc := BLC.CreateBlockchainWithGensisBlock("chaors")
    //utxos := blc.UnUTXOs("chaors")
    //fmt.Println(utxos)
}

创建好创世区块后,执行第一次转账.
chaors(25btc) -->ww(10) + chaors(15)

Main_test1.png
Main_test2.png

总结

当前的交易函数是人工硬编码,下次再具体实现。

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

.
.
.
.

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

---------------------------------------------20180703 23:47
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容