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

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

资料

上一篇 下一篇

猜你喜欢

热点阅读