IT技术区块链研习社金马带你定投区块链

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

2018-03-21  本文已影响52人  even_366

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签名确认 输入减去输出,为交易服务费

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

资料

上一篇下一篇

猜你喜欢

热点阅读