4.教你打造最简比特币之交易

开发语言:Go语言

本教程是学习Jeiwan的博客后的学习笔记,代码实现也参考它的为主,精简了叙述并在适当位置添加了一些必备的小知识和适当的代码注释。

本教程是为了逐步教你设计一款简化的区块链原型币。通过我们不断添加功能,完成一个可交易的最简比特币。

本节我们将实现支持交易功能的最简比特币

  1. 单机版,区块仅支持保存信息✅
  2. 工作量证明✅
  3. 持久化✅
  4. 交易功能,区块支持保存交易

比特币的交易

传统的交易系统是基于账户的系统,意味着有两个表:

  1. 账户表(accounts),存用户信息和余额
  2. 交易记录(transactions),存从一个账户到另一个账户的交易。
    在比特币的世界里,是没有账户的概念,这意味着没有余额。比特币仅仅是一大堆交易的集合而已。比特币里只有交易。

交易的结构

在比特币的世界里,一个交易由输入和输出组成,还有一个交易的ID

type Transaction struct {
    ID   []byte
    Vin  []TXInput
    Vout []TXOutput
}

每一笔输入都会引用前一笔交易的输出作为来源,(作为挖矿奖励的coinbase交易是特例)。输出是比特币真正存储的地方。若这个输出没有别的输入引用,则可以认为比特币在这个输出的地址的人的手里。

4.教你打造最简比特币之交易_第1张图片
image

总结:

  1. 一个输入肯定有对应的上一个输出(coinbase除外)
  2. 输入可以来源于多个不同的交易
  3. 有些输出没有链接到下一个输入,则比特币在这个输出的地址的人手里

比特币里的交易,从代码执行的层面上看,是将一个币值锁在输出里,而只能被这个输出的人解锁并使用它。

交易的输出

type TXOutput struct {
    Value        int
    ScriptPubKey string
}

这里的Value就是币值,而ScriptPubKey就是把币值锁在这个输出里的数学谜题。

交易的执行,是靠比特币内部的脚本语言实现的,锁和解锁交易都由它完成。你感兴趣的话可以阅读它

比特币的最小单位是一个中本聪,等于0.00000001个比特币。

我们暂时还没涉及到“地址”的概念,会忽略与之相关的逻辑代码。ScriptPubKey暂时保存任意字符串

值得注意的是,一个交易的输出不可分割,也就是你不能只花交易的一部分,要么全花掉要么不花,但是可以用找零的方式间接的花部分的币。比如,你有一个5块的输出,你只想花掉1块,那么花5块找零4块即可,也就是这4块钱的收款地址填你的地址。

交易的输入

type TXInput struct {
    Txid      []byte
    Vout      int
    ScriptSig string
}

Txid表示上一个交易的输出所在的交易ID。
Vout表示该输出的在上一个交易中的索引位置
ScriptSig提供了输出ScriptPubKey需要的数据。如果这个数据正确,则输出能被正确锁定,也就是币能被花掉。这个机制让人们不能随便花别人的币。我们暂时还没涉及到“地址”的概念,会忽略与之相关的逻辑代码。暂时,ScriptSig存任意字符串。

总结:

  1. 输出是比特币保存的地方。
  2. 每一个输出有一个解锁条件ScriptPubKey,决定了它怎么被解锁。
  3. 每一个交易至少包含一个输入和一个输出
  4. 输入通过提供ScriptSig来解锁这个输入所引用输出,如果正确则可以使用这个输入所指向的输出。

先有鸡还是先有蛋?

每个输入都有引用一个输出作为来源,而输出又是输入产生的。那么最早的那个输出从哪来来?

答案是:一个特殊的交易--Coinbase交易,又叫做创币交易。Coinbase交易是产生比特币的来源,他只有输出,通常是输出到矿工的地址。它作为提供算力和网络等计算机设施的比特币矿工所能得到的奖励。

func NewCoinbaseTX(to, data string) *Transaction {
    if data == "" {
        data = fmt.Sprintf("Reward to '%s'", to)
    }

    txin := TXInput{[]byte{}, -1, data}
    txout := TXOutput{subsidy, to}
    tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
    tx.SetID()

    return &tx
}

由于没有输入,这里Txid填空和Vout填-1。由于没有上一个交易的输出作为这个输入的来源,ScriptSig也无所谓填什么。

subsidy 是创币奖励的金额。比特币的创币奖励最初是50个,每210000个区块减半。

将交易存入区块链

从现在开始,我们的区块升级了,可以存交易了。不再是只存一点基本信息了。

type Block struct {
    Timestamp     int64
    Transactions  []*Transaction
    PrevBlockHash []byte
    Hash          []byte
    Nonce         int
}

相应的用到Block的也要修改

func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
    block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
    ...
}

func NewGenesisBlock(coinbase *Transaction) *Block {
    return NewBlock([]*Transaction{coinbase}, []byte{})
}

对应的创造区块链的也修改

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))
        err = b.Put(genesis.Hash, genesis.Serialize())
        ...
    })
    ...
}

相应的工作量证明也需要加入了交易的哈希

func (pow *ProofOfWork) prepareData(nonce int) []byte {
    data := bytes.Join(
        [][]byte{
            pow.block.PrevBlockHash,
            pow.block.HashTransactions(), // This line was changed
            IntToHex(pow.block.Timestamp),
            IntToHex(int64(targetBits)),
            IntToHex(int64(nonce)),
        },
        []byte{},
    )

    return data
}

这里的哈希我们采用将每个交易取哈希,然后拼接起来后整体再做一次哈希。

注意:这个方法和比特币的不同,比特币对于交易的哈希使用的是Merkle Tree的方法,这个方法能快速验证一个交易是否在区块当中,而且验证时不需要下载整个区块的交易。

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[:]
}

未花费交易(UTXO)

未花费交易(UTXO)表示的是每个没有对应输入引用的输出,如之前的图所示就是这几个。

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. tx4, output 0;

当我们查看某个地址余额时,只需要能被那个账户解锁的UTXO。这里我们的上锁和解锁的操作暂时实现如下,仅仅比对地址一致即可,后续会实现用私钥来上锁解锁

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
    return in.ScriptSig == unlockingData
}

func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
    return out.ScriptPubKey == unlockingData
}

查找UTXO的余额的代码如下

func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
  var unspentTXs []Transaction
  spentTXOs := make(map[string][]int) // 键是交易的ID,值是输出索引的数组
  bci := bc.Iterator()

  for {
    block := bci.Next()//遍历每一个区块,顺序是从后往前遍历,从最新的区块到最老的区块

    for _, tx := range block.Transactions {//遍历每一笔交易
      txID := hex.EncodeToString(tx.ID)

    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) { //如果没被引用则加入UTXO
          unspentTXs = append(unspentTXs, *tx)//注意:这里暗含一个假设是一个交易里,一个地址只有一个输出,否则会重复添加这个交易,在FindUTXO里就会重复添加输出TXOutput
        }
      }

      if tx.IsCoinbase() == false {
        for _, in := range tx.Vin {  //每一个输入都表明有一个输出被使用了,则加入已被使用的map
          if in.CanUnlockOutputWith(address) {
            inTxID := hex.EncodeToString(in.Txid)
            spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
          }
        }
      }
    }

    if len(block.PrevBlockHash) == 0 {
      break
    }
  }

  return unspentTXs
}

注意遍历的顺序是从最新的区块到最老的区块,每个区块里的每一个交易的每一个输出都检查是否已经被引用,如果没有就加入UTXOs,然后每个输入肯定都有一个输出被引用,则加入spentTXOs。

接下来提取出每个交易里的输出

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
}

然后我们就能计算余额了

func (cli *CLI) getBalance(address string) {
    bc := NewBlockchain(address)
    defer bc.db.Close()

    balance := 0
    UTXOs := bc.FindUTXO(address)

    for _, out := range UTXOs {
        balance += out.Value
    }

    fmt.Printf("Balance of '%s': %d\n", address, balance)
}

发送币

如果你想发送币,你需要创建一个交易,并放入区块,并计算区块的工作量。
但是首先,你需要查找你能花的币

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
}

然后才是创建你的交易

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)

        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
}

打造完交易后,我们修改挖矿的方法

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
    ...
    newBlock := NewBlock(transactions, lastHash)
    ...
}

最后,我们可以写出发送币的代码

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!")
}

终于来到我们的测试环节,在控制台输入指令。这里我的挖矿奖励是10个币

blockchain_go getbalance -address Ivan
Balance of 'Ivan': 10

试一试发送币,

 blockchain_go send -from Ivan -to Pedro -amount 6
00000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37

Success!

再测试两个账户的币余额

blockchain_go getbalance -address Ivan
Balance of 'Ivan': 4

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 6

注意,创建Blockchain 时,Ivan有了10块钱,发送币时又创建了Block没有加入coinbase奖励,所以转6个给Pedro,剩下4个。

参考:

Building Blockchain in Go. Part 4: Transactions 1,jiewan

源码

https://github.com/linxinzhe/go-simple-coin/tree/4_transaction

下一节:

全系列:

  1. 教你打造最简比特币之基本原型
  2. 教你打造最简比特币之工作量证明
  3. 教你打造最简比特币之持久化
  4. 教你打造最简比特币之交易
  5. 未完待续

关于我:
linxinzhe,策略工程师,在某银行从事IT策略研究和规划,领域:企业数字化、企业架构、公司金融、金融科技。文集:linxinzhe.cn

你可能感兴趣的:(4.教你打造最简比特币之交易)