美文网首页IT技术区块链研习社金马带你定投区块链
Go实现区块链(四)---交易事物(一)

Go实现区块链(四)---交易事物(一)

作者: even_366 | 来源:发表于2018-03-21 17:26 被阅读52次

    1.前言

    上一篇我知道了区块链如何持久化存储,接下来我们将开始实现区块链中交易是如何产生的如何防止被串改,如何在网络中分布式记账。我们将交易分成两部分:交易实现一般机制,后面将实现网络、奖励机制等。

    2.知识准备

    知识点 学习网页 特性
    bitcoin交易 交易 不可串改
    Coinbase 创世块交易信息 创世块交易

    3.基本交易过程

    在区块链中每次发生交易,用户需要将新的交易记录写到比特币区块链网络中,等待网络确认为交易完成。每个交易包括了一些输入和一些输出,未经使用的交易的输出(Transaction Outputs,UTXO)可以被新的交易引用作为合法输入,被使用过的交易的输出(Spent Transaction Outputs,STO)则无法被引用作为合法输入。

    注意:这里的比特币交易与我们传统的金钱付款交易是不同的,并没有账号、没有余额等。详情参考

    4.代码实现

    交易:

    //交易事物
    type Transaction struct {
        ID   []byte     //交易hash
        Vin  []TXInput  //事物输入
        Vout []TXOutput //事物输出
    }
    

    流程图:


    交易引用图

    注意:

    • 有些输出和输入无关。
    • 在一个交易中,投入可以参考多个交易的输出。
    • 输入必须引用输出。

    比特币中没有这样的概念。事务只是用脚本锁定值,只能由锁定它的人解锁。

    交易输出:

    //一个事物输出
    type TXOutput struct {
        Value int       //值
        ScriptPubKey string //解锁脚本key
    }
    

    实际上,它是存储“硬币”的输出(注意Value上面的字段)。而存储意味着用一个拼图锁定它们,这是存储在ScriptPubKey。在内部,比特币使用称为脚本的脚本语言,用于定义输出锁定和解锁逻辑。这个语言很原始(这是故意的,以避免可能的黑客和滥用),但我们不会详细讨论它。你可以在本章知识点bitcoin交易详细解释。

    在比特币中,价值领域存储satoshis的数量,而不是BTC的数量。甲聪是100000000分之1一个比特币(0.00000001 BTC)的,因此,这是货币的比特币的最小单位(如百分比)。

    由于我们没有实现地址,现在我们将避免整个脚本相关的逻辑。ScriptPubKey将存储任意字符串(用户定义的钱包地址)。

    交易输入:

    //一个事物输入
    type TXInput struct {
        Txid []byte //交易ID的hash
        Vout int    //交易输出
        ScriptSig string //解锁脚本
    }
    

    如前所述,输入引用前一个输出:Txid存储此类事务的ID,并Vout在事务中存储输出的索引。ScriptSig是一个提供数据以在输出中使用的脚本ScriptPubKey。如果数据是正确的,输出可以被解锁,并且它的值可以被用来产生新的输出; 如果不正确,则输出中不能引用输出。这是保证用户不能花钱属于其他人的硬币的机制。

    同样,由于我们还没有实现地址,ScriptSig因此将只存储任意用户定义的钱包地址。我们将在下一篇文章中实现公钥和签名检查。

    我们总结一下。产出是储存“硬币”的地方。每个输出都带有一个解锁脚本,它决定了解锁输出的逻辑。每个新事务都必须至少有一个输入和输出。输入引用前一个事务的输出,并提供ScriptSig输出的解锁脚本中使用的数据(字段),以解除锁定并使用其值创建新的输出。

    coinbase交易:
    上面我们知道输入参考输出逻辑,而输出又参考了输入。这样就产生了我们常见的一个问题:先有鸡还是先有蛋呢?

    当矿工开始挖矿时,它会添加一个coinbase交易。coinbase交易是一种特殊类型的交易,不需要以前存在的输出。它无处不在地创造产出(即“硬币”)。没有鸡的鸡蛋。这是矿工获得开采新矿区的奖励。

    如您所知,区块链开始处有起始块。这个区块在区块链中产生了第一个输出。由于没有以前的交易并且没有这样的输出,因此不需要先前的输出。

    我们来创建一个coinbase事务:

    //创建Coinbase事物
    func NewCoinbaseTX(to,data string) *Transaction  {
        if data==""{
            data=fmt.Sprintf("Reward to '%s'",to)
        }
        //这里Vout-1 data:const genesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
        txin :=TXInput{[]byte{},-1,data}
        //subsidy是奖励的金额  
        txout := TXOutput{subsidy, to}
        tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
        //设置32位交易hash
        tx.SetID()
        return &tx
    }
    
    //设置交易ID hash
    func (tx *Transaction) SetID(){
        var encoded bytes.Buffer
        var hash [32]byte //32位的hash字节
    
        enc := gob.NewEncoder(&encoded)
        err := enc.Encode(tx)
        if err != nil {
            log.Panic(err)
        }
        //将交易信息sha256
        hash = sha256.Sum256(encoded.Bytes())
        //生成hash
        tx.ID = hash[:]
    }
    

    一个coinbase交易只有一个输入。在我们的实现中它Txid是空的,Vout等于-1。另外,coinbase事务不会存储脚本ScriptSig。相反,任意数据存储在那里。

    在比特币中,第一个coinbase交易包含以下信息:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。查看知识点Coinbase可以知道。

    subsidy是奖励的金额。在比特币中,这个数字没有存储在任何地方,只根据块的总数进行计算:块的数量除以210000。挖掘创世纪块产生50 BTC,每210000块奖励减半。在我们的实现中,我们会将奖励作为常量存储(至少现在是😉)。

    在区块链中存储交易:
    我们将开始区块里面的data改成transactions

    //区块结构
    type Block struct {
        Hash          []byte //hase值
        Transactions []*Transaction//交易数据
        PrevBlockHash []byte //存储前一个区块的Hase值
        Timestamp     int64  //生成区块的时间
        Nonce         int    //工作量证明算法的计数器
    }
    

    对应的NewBlock,NewGensisBlock也应该修改:

    //生成一个新的区块方法
    func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block{
        //GO语言给Block赋值{}里面属性顺序可以打乱,但必须制定元素 如{Timestamp:time.Now().Unix()...}
        block := &Block{Timestamp:time.Now().Unix(), Transactions:transactions, PrevBlockHash:prevBlockHash, Hash:[]byte{},Nonce:0}
    
        //工作证明
        pow :=NewProofOfWork(block)
        //工作量证明返回计数器和hash
        nonce, hash := pow.Run()
        block.Hash = hash[:]
        block.Nonce = nonce
        return block
    }
    
    //创建并返回创世纪Block
    func  NewGenesisBlock(coinbase *Transaction) *Block {
        return NewBlock([]*Transaction{coinbase}, []byte{})
    }
    
    

    blockchain:

    / 创新一个新的区块数据
    func CreateBlockchain(address string) *Blockchain {
        ...
        err = db.Update(func(tx *bolt.Tx) error {
            cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
            genesis := NewGenesisBlock(cbtx)
            b, err := tx.CreateBucket([]byte(blocksBucket))
            if err != nil {
                log.Panic(err)
            }
            err = b.Put(genesis.Hash, genesis.Serialize())
            ...
    }
    

    这里,函数将获得一个地址,该地址将获得挖掘创世块的奖励。(我们这里奖励为10)

    工作量证明:

    Proof-of-Work算法必须考虑存储在块中的事务,以保证区块链作为事务存储的一致性和可靠性。所以现在我们必须修改ProofOfWork.prepareData方法:

    //将区块体里面的数据转换成一个字节码数组,为下一个区块准备数据
    func (pow *ProofOfWork) prepareData(nonce int) []byte {
        //注意一定要将原始数据转换成[]byte,不能直接从字符串转
        data := bytes.Join(
            [][]byte{
                pow.block.PrevBlockHash,
                pow.block.HashTransactions(),
                utils.IntToHex(pow.block.Timestamp),
                utils.IntToHex(int64(targetBits)),
                utils.IntToHex(int64(nonce)),
            },
            []byte{},
        )
        return data
    }
    

    将data改成hashTransactions:

    //返回块状事务的hash
    func (b *Block) HashTransactions() []byte {
      var txHashes [][]byte
      var txHash [32]byte
      for _, tx := range b.Transactions {
            txHashes = append(txHashes, tx.ID)
        }
        txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))
    
        return txHash[:]
    }
    

    我们使用散列作为提供数据的唯一表示的机制。我们希望块中的所有事务都由一个散列唯一标识。为了达到这个目的,我们得到每个事务的哈希值,连接它们,并获得连接组合的哈希值。

    比特币使用更复杂的技术:它将所有包含在块中的事务表示为Merkle树,并在Proof-of-Work系统中使用树的根散列。这种方法允许快速检查块是否包含某个事务,只有根散列并且不下载所有事务。(后续会详细讲解Merkle算法)

    好了我们现在尝试一下CreateBlockchain:
    编译:

    C:\go-worke\src\github.com\study-bitcoin-go>go build github.com/study-bitcoin-go
    

    执行createblockchain命令:

    C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createblockchain -address even
    
    

    输出:

    Dig into mine  00000860adb64e2ca9c83d0f665f7ec3148ec9d32b64cb97f2481712c4d94d79
    
    Done!
    

    目前为止我们实现开采创世块奖励。但我们要如何实现查询余额呢?

    未使用交易输出
    我们需要找到所有未使用的交易输出(UTXO)。未使用意味着这些输出在任何输入中都未被引用。在上图中,这些是:

    1. tx0,输出1;
    2. tx1,输出0;
    3. tx3,输出0;
    4. tx4,输出0。

    当然,当我们检查余额时,我们不需要所有这些,但只有那些可以用我们拥有的密钥解锁的(当前我们没有实现密钥并且将使用用户定义的地址)。首先,我们来定义输入和输出上的锁定 - 解锁方法:

    //通过检查地址是否启动了事务
    func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
        return in.ScriptSig == unlockingData
    }
    //检查输出是否可以使用所提供的数据进行解锁
    func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
        return out.ScriptPubKey == unlockingData
    }
    

    这里我们只是比较脚本字段unlockingData。在我们实现基于私钥的地址后,这些作品将在未来的文章中得到改进。
    下一步 - 查找包含未使用产出的交易 - 相当困难:

    //查询未处理的事务
    func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
        var unspentTXs []Transaction //未处理的事务
        spentTXOs := make(map[string][]int)
        bci := bc.Iterator()
    
        for {
            block := bci.Next()
            for _,tx := range block.Transactions {
                txID := hex.EncodeToString(tx.ID)  //交易ID转换成string
            Outputs:
                for outIdx, out := range tx.Vout {
                    // Was the output spent?
                    if spentTXOs[txID] != nil {
                        //检查一个输出是否已经在输入中被引用
                        for _, spentOut := range spentTXOs[txID] {
                            if spentOut == outIdx {
                                continue Outputs
                            }
                        }
                    }
                    //由于交易存储在块中,因此我们必须检查区块链中的每个块。我们从输出开始:
                    if out.CanBeUnlockedWith(address) {
                        unspentTXs = append(unspentTXs, *tx)
                    }
                }
    
                //我们跳过输入中引用的那些(它们的值被移到其他输出,因此我们不能计数它们)。
                // 在检查输出之后,我们收集所有可能解锁输出的输入,并锁定提供的地址(这不适用于coinbase事务,因为它们不解锁输出)
                if tx.IsCoinbase() == false {
                    for _, in := range tx.Vin {
                        if in.CanUnlockOutputWith(address) {
                            inTxID := hex.EncodeToString(in.Txid)
                            spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
                        }
                    }
                }
            }
            if len(block.PrevBlockHash) == 0 {
                break
            }
        }
        return unspentTXs
    }
    

    该函数返回一个包含未使用输出的事务列表。为了计算余额,我们需要一个函数来处理事务并仅返回输出:

    //发现并返回所有未使用的事务输出
    func (bc *Blockchain) FindUTXO(address string) []TXOutput {
        var UTXOs []TXOutput
        //未使用输出的事务列表
        unspentTransactions := bc.FindUnspentTransactions(address)
        //查找
        for _, tx := range unspentTransactions {
            for _, out := range tx.Vout {
                ///检查输出是否可以使用所提供的数据进行解锁
                if out.CanBeUnlockedWith(address) {
                    UTXOs = append(UTXOs, out)
                }
            }
        }
        return UTXOs
    }
    

    客户端cli getbalance命令:

    //查询余额
    func (cli *CLI) getBalance(address string) {
        bc := block.NewBlockchain(address)
        defer block.Close(bc)
    
        balance := 0
        //查询所有未经使用的交易地址
        UTXOs := bc.FindUTXO(address)
        //算出未使用的交易地址的value和
        for _, out := range UTXOs {
            balance += out.Value
        }
        fmt.Printf("Balance of '%s': %d\n", address, balance)
    }
    

    账户余额是由账户地址锁定的所有未使用的交易输出值的总和。

    现在我们检查一下地址为even的钱:
    重新编译后

    C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address even
    Balance of 'even': 10
    

    这就是第一笔钱,接下来我们需要实现给一个地址转币

    现在,我们要发送一些硬币给别人。为此,我们需要创建一个新的事务,将它放在一个块中,然后挖掘块。到目前为止,我们只实现了coinbase交易(这是一种特殊类型的交易),现在我们需要一个一般交易:

    //创建一个新的未经使用的交易输出
    func NewUTXOTransaction(from, to string, amount int, bc *Blockchain)   *Transaction{
        var inputs []TXInput
        var outputs []TXOutput
        //查询发币地址所未经使用的交易输出
        acc, validOutputs := bc.FindSpendableOutputs(from, 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, from}
                inputs = append(inputs, input)
            }
        }
        // Build a list of outputs
        outputs = append(outputs, TXOutput{amount, to})
        if acc > amount {
            outputs = append(outputs, TXOutput{acc - amount, from}) // a change
        }
        tx := Transaction{nil, inputs, outputs}
        tx.SetID()
        return &tx
    }
    

    在创建新的输出之前,我们首先必须找到所有未使用的输出并确保它们存储足够的值。这是什么FindSpendableOutputs方法。之后,为每个找到的输出创建一个引用它的输入。接下来,我们创建两个输出:

    1. 一个与接收器地址锁定的。这是硬币实际转移到其他地址。
    2. 一个与发件人地址锁定在一起。这是一个变化。只有在未使用的输出持有比新事务所需的更多价值时才会创建。记住:输出是不可分割的。

    FindSpendableOutputs方法基于FindUnspentTransactions我们之前定义的方法:

    func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int)  {
        unspentOutputs := make(map[string][]int)
        unspentTXs := bc.FindUnspentTransactions(address)
        accumulated := 0
    
    Work:
        for _, tx := range unspentTXs {
            txID := hex.EncodeToString(tx.ID)
            for outIdx, out := range tx.Vout {
                if out.CanBeUnlockedWith(address) && accumulated < amount {
                    accumulated += out.Value
                    unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)
    
                    if accumulated >= amount {
                        break Work
                    }
                }
            }
        }
        return accumulated, unspentOutputs
    }
    

    该方法迭代所有未使用的事务并累积其值。当累计值大于或等于我们要转移的金额时,它停止并返回按事务ID分组的累计值和输出索引。我们不想花更多的钱。

    现在我们可以修改该Blockchain.MineBlock方法:

    //开采区块
    func (bc *Blockchain) MineBlock(transactions []*Transaction)  {
        var lastHash  []byte//最新一个hash
        err := bc.db.View(func(tx *bolt.Tx) error {
            b := tx.Bucket([]byte(blocksBucket))
            lastHash = b.Get([]byte("l"))
    
            return nil
        })
        if err != nil {
            log.Panic(err)
        }
        //创造一个新区块
        newBlock := NewBlock(transactions, lastHash)
        //修改"l"的hash
        err = bc.db.Update(func(tx *bolt.Tx) error {
            b := tx.Bucket([]byte(blocksBucket))
            err := b.Put(newBlock.Hash, newBlock.Serialize())
            if err != nil {
                log.Panic(err)
            }
            err = b.Put([]byte("l"), newBlock.Hash)
            if err != nil {
                log.Panic(err)
            }
            bc.tip = newBlock.Hash
    
            return nil
        })
    }
    

    cli添加send方法

    func (cli *CLI) send(from, to string, amount int) {
        bc := NewBlockchain(from)
        defer bc.db.Close()
    
        tx := NewUTXOTransaction(from, to, amount, bc)
        bc.MineBlock([]*Transaction{tx})
        fmt.Println("Success!")
    }
    

    发送硬币意味着创建一个交易并通过挖掘一个块将其添加到区块链。但比特币不会立即做到这一点(就像我们一样)。相反,它将所有新事务放入内存池(或mempool)中,并且当矿工准备开采块时,它将从mempool获取所有事务并创建候选块。交易只有在包含它们的区块被挖掘并添加到区块链时才会被确认。

    现在我们来试试发币:

    C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go send  -from even -to jim -amount 3
     Mining the block containing "S4t�.U��̧�ϤH��vWE���[�P�╔���"
     Dig into mine  00000cde90398b754eebe6d7820dab6e6260ae724712b72706846ec6d331fe2c
    
    Success!
    

    分别查询even、tom钱包:

    C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address even
    Balance of 'even': 7
    
    C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address jim
    Balance of 'jim': 3
    

    好了目前我们实现了交易功能。缺少:

    1. 地址。我们还没有真实的,基于私钥的地址。
    2. 奖励。采矿块绝对没有利润!
    3. UTXO设置。达到平衡需要扫描整个区块链,当区块数量很多时可能需要很长时间。此外,如果我们想验证以后的交易,可能需要很长时间。UTXO集旨在解决这些问题并快速处理交易。
    4. 内存池。这是交易在打包成块之前存储的地方。在我们当前的实现中,一个块只包含一个事务,而且效率很低。

    后续降会实现地址、钱包、挖矿奖励、网络等。

    5.比特币交易示例总结

    交易 目的 输入 输出 签名 差额
    T0 A转给B 他人向A交易的输出 B账号可以使用该交易 A签确认 输入减去输出,为交易服务费
    T1 B转给C T0的输出 C账户可以使用该交易 B签名确认 输入减去输出,为交易服务费
    ··· X转给Y 他人向X交易的输出 Y账户可以使用该交易 X签名确认 输入减去输出,为交易服务费

    这就是简单交易流程,我们以上代码并没有实现挖矿奖励。后续将实现钱包,优化查询交易,挖矿奖励、网络。

    资料

    相关文章

      网友评论

        本文标题:Go实现区块链(四)---交易事物(一)

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