utxo模型_UTXO模型实现

fb09012eedb1e75c44242ed561af2271.png

之前我们有两篇文章已经介绍了如何开发一个简易的区块链,并实现PoW这样的共识机制以及区块链数据持久化和数据加密相关的内容,今天我们继续动手来实践区块链的关键技术点--UTXO模型。

说到UTXO模型,其实是Unspent Transaction Outputs的缩写,意在表示未花费的交易输出。这样翻译过来仍然很别扭,它的核心思想其实是为了取代传统的账户模型,将去中心化账本进行到底。UTXO的设计理念是任何一笔交易都有输入和输出,基于这样的链式存储结果下,任何一个账户的余额都可以通过追述它的输入和输出计算得到。

中本聪为什么要选择UTXO模型,而不选用传统的账户模型呢?它的优点是原子性,或成功或失败,无中间状态,因为天然的原子性,所以对并发支持好。当然也有一些它的缺点,比如太复杂了,实现起来较为复杂,也无法承载更多的内容,比如以太坊的智能合约就无法通过UTXO来实现,最后小v选择了传统的账户模型。

接下来,我们介绍如何通过Go语言来实现UTXO模型,UTXO模型与交易息息相关,我们来定义交易的模型,以及交易的输入和输出。

// TXInput represents a transaction input
type TXInput struct {
  Txid      []byte //交易id
  Vout      int  // 输出的编号
  ScriptSig string //执行脚本
}

// TXOutput represents a transaction output
type TXOutput struct {
  Value        int //金额
  ScriptPubKey string //公钥
}

// Transaction represents a Bitcoin transaction
type Transaction struct {
  ID   []byte  //交易hash
  Vin  []TXInput //交易的输入 - 付款方
  Vout []TXOutput //交易的输出 - 收款方 
}

对于每个区块,都需要存储交易信息,我们将之前编写的区块结构进行修改

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

我们需要针对区块中的交易信息进行一些处理,这里主要是join就可以了,便于计算hash值。

// HashTransactions returns a hash of the transactions in the block
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[:]
}

在挖矿前的prepare也需要把交易信息加进去

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

  return data
}

接下来,我们全力处理交易的事情,但在此之前我们先做3个辅助函数:

·IsCoinbase 判断是否为系统奖励

// IsCoinbase checks whether the transaction is coinbase
func (tx Transaction) IsCoinbase() bool {
  return len(tx.Vin) == 1 && len(tx.Vin[0].Txid) == 0 && tx.Vin[0].Vout == -1
}

·CanUnlockOutputWith 是否可以解锁TXInput

// CanUnlockOutputWith checks whether the address initiated the transaction
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
  return in.ScriptSig == unlockingData
}

·CanBeUnlockedWith是否可以解锁TXOutput

// CanBeUnlockedWith checks if the output can be unlocked with the provided data
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
  return out.ScriptPubKey == unlockingData
}

先生成一个最简单的家里,系统奖励coinbase

// NewCoinbaseTX creates a new coinbase transaction
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
}

很显然,系统奖励只需要指定奖励给谁,奖励多少就行了,data记录了奖励的备注信息。

对于要找到未到使用的交易输出就有点麻烦了,针对所有的非coinbase,我们需要找到可以被输入地址解锁的txin,一旦找到了,我们将它记录下来,在这里我们需要搞一个string->[]int这样的数据结构,string代表交易ID,整形数组存放txout的序号。

spentTXOs := make(map[string][]int)

我们需要遍历区块链,找到所有的交易信息,将这个map填上

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

得到这样的map之后,我们动态的判断该map中的txout是否已经被划出了,如果没有被划出,并且可以被给定地址解锁,那么也就找到了我们的目标,将它们作为数组返回。

// FindUnspentTransactions returns a list of transactions containing unspent outputs
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)

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

      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
}

接下来可以针对上述函数的返回结果进行处理,得到UTXO

// FindUTXO finds and returns all unspent transaction outputs
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
}

再编写一个计算余额充足与否的函数

// FindSpendableOutputs finds and returns unspent outputs to reference in inputs
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
}

接下来我们生成一个UTXO的transaction:

// NewUTXOTransaction creates a new transaction
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
}

生成交易前先查找确保有足够的余额,之后生成一个新的交易。在我们创世块创建的时候,需要增加一个coinbase的交易。

        cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
    genesis := NewGenesisBlock(cbtx)

再增加一个挖矿的函数,此时传入的函数注意都变成交易数组信息。

// MineBlock mines a new block with the provided transactions
func (bc *Blockchain) MineBlock(transactions []*Transaction) {
  var lastHash []byte

  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)

  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中增加客户端的余额查询功能,其实UTXO找到了就是一个累加的过程。

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': %dn", address, balance)
}

实现一个交易发送也很容易了

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

将我们的cli帮助进行更新

func (cli *CLI) printUsage() {
  fmt.Println("Usage:")
  fmt.Println("  getbalance -address ADDRESS - Get balance of ADDRESS")
  fmt.Println("  createblockchain -address ADDRESS - Create a blockchain and send genesis block reward to ADDRESS")
  fmt.Println("  printchain - Print all the blocks of the blockchain")
  fmt.Println("  send -from FROM -to TO -amount AMOUNT - Send AMOUNT of coins from FROM address to TO")
}

剩下的也就是在cli的Run中把这些参数支持,加以调用的问题了,接下来不再详述!本文主要是给大家介绍UTXO的逻辑,从Go语言的实现逻辑来看,代码确实较为复杂,对于要理解代码的朋友简易要搞清楚逻辑,再分析代码。

关注更多请锁定gzh:柏链学习社

根据区块链网络中心化程度的不同,分化出3种不同应用场景下的区块链:(1)全网公开,无用户授权机制的区块链,称为公有链;(2)允许授权的节点加入网络,可根据权限查看信息,往往被用于机构间的区块链,称为联盟链或行业链;(3)所有网络中的节点都掌握在一家机构手中,称为私有链。联盟链和私有链也统称为许可链,公有链称为非许可链。 公有区块链系统 公有链中,任何节点无须任何许可便可随时加入或脱离网络。从最早的比特币系统人手介绍公有链系统的发展现状。点对点电子现金系统:比特币与传统分布式系统的C/S , B/S或三层架构不同,比特币系统基于P2P网络,所有节点对等,且都运行同样的节点程序。节点程序总体上分为两部分:一部分是前台程序,包括钱包或图形化界面;另一部分是后台程序,包括挖矿、区块链管理、脚本引擎及网络管理等。区块链管理:涉及初始区块链下载、连接区块、断开区块、校验区块和保存区块,以及发现最长链条的顶区块。内存池管理:即交易池管理。节点将通过验证的交易放在一个交易池中,并准备好将其放入下一步挖到的区块中。邻接点管理:当一个新比特币节点初始启动时,它需要发现网络中的其他节点,并与至少一个节点连接。共识管理:比特币中的共识管理包括挖矿、区块验证和交易验证规则。比特币采用PoW共识机制,依赖机器进行哈希运算来获取记账权,同时每次达成共识需要全网共同参与运算,允许全网50%节点出错。密码模块:比特币采用RIMEMD和SHA-256算法及Base-58编码生成比特币地址。签名模块:比特币采用椭圆曲线secp256k1及数字签名算法ECDSA来实现数字签名并生成公钥。脚本引擎:比特币的脚本语言是一种基于堆栈的编程脚本,共有256个指令,是非图灵完备的运算平台,没有能力计算任意带复杂功能的任务。本课程从零到一带领你实践一个小型公链。  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值