之前我们有两篇文章已经介绍了如何开发一个简易的区块链,并实现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:柏链学习社