前言
上一章我们介绍了区块链的PoW共识机制,理解了区块是如何合法的加入到区块链中。在本章我们将讲解区块中的数据是如何保存的以及UTXO模型实现。
UTXO模型
这部分大家自行查找相关资料吧,不多说了
tx信息
我们在goblockchain项目下新建文件夹叫transaction,然后在其中建立两个文件,分别为transaction.go和inoutput.go,如下图。
在utxo中,交易信息被切分为两部分,分别为input与output。在inoutput中我们构造结构体,使得TxOutput代表交易信息的Output,TxInput代表交易信息的Input。
package transaction
import "bytes"
type TxOutput struct {
Value int
ToAddress []byte
}
type TxInput struct {
TxID []byte
OutIdx int
FromAddress []byte
}
TxOutput包含Value与ToAddress两个属性,前者是转出的资产值,后者是资产的接收者的地址。TxInput包含的TxID用于指明支持本次交易的前置交易信息,OutIdx是具体指明是前置交易信息中的第几个Output,FromAddress就是资产转出者的地址。
然后我们转到transaction.go,构造结构体。
type Transaction struct {
ID []byte
Inputs []TxInput
Outputs []TxOutput
}
Transaction由自身的ID值(其实就是哈希值),一组TxInput与一组TxOutput构成。TxInput用于标记支持我们本次转账的前置的交易信息的TxOutput,而TxOutput记录我们本次转账的amount和Reciever。你可能对于为什么要记录一组TxOutput有疑惑,这是因为在寻找到了足够多的未使用的TxOutput(后面全部简称UTXO)后,其资产总量可能大于我们本次交易的转账总量,我们可以将找零计入本次的TxOutput中,设置其流入方向就是本次交易的Sender(一定要好好理解!),这样就实现了找零。
这里我们首先实现计算每个transaction的哈希值的功能,如下。
func (tx *Transaction) CalculateTXHash() []byte {
var buffer bytes.Buffer
var hash [32]byte
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(tx)
utils.Handle(err)
hash = sha256.Sum256(buffer.Bytes())
return hash[:]
}
func (tx *Transaction) SetID() {
tx.ID = tx.CalculateTXHash()
}
gob 是 Go 语言内置的用于序列化和反序列化 Go 值的包,可以将结构体、数组、切片等数据类型编码成二进制数据,然后将其写入缓冲区中。这里使用 gob.NewEncoder() 函数创建一个编码器,并将其与 encoded 缓冲区关联起来。
创建tx信息
第一笔交易是凭空产生的,这里我们要自己写
func BaseTx(toaddress []byte) *Transaction {
txIn := TxInput{[]byte{}, -1, []byte{}}
txOut := TxOutput{constcoe.InitCoin, toaddress}
tx := Transaction{[]byte("This is the Base Transaction!"),
[]TxInput{txIn}, []TxOutput{txOut}}
return &tx
}
func (tx *Transaction) IsBase() bool {
return len(tx.Inputs) == 1 && tx.Inputs[0].OutIdx == -1
}
别忘了去设置一下初始的数量
package constcoe
const (
Difficulty = 12
InitCoin = 1000// This line is new
)
接下来我们要在inoutput.go中写几个验证函数,后面要用
func (in *TxInput) FromAddressRight(address []byte) bool {
return bytes.Equal(in.FromAddress, address)
}
func (out *TxOutput) ToAddressRight(address []byte) bool {
return bytes.Equal(out.ToAddress, address)
}
好了,差不多了,下面我们将区块链的代码重构
代码重构
修改block结构体
type Block struct {
Timestamp int64
Hash []byte //区块hash值就是其ID
PrevHash []byte
Nonce int64
Target []byte
Transactions []*transaction.Transaction
}
首先是返回区块哈希的函数,我们再创建一个函数来协助处理区块中交易信息的序列化并修改sethash方法。
func (b *Block) BackTXSummary() []byte {
txIDs := make([][]byte, 0)
for _, tx := range b.Transactions {
txIDs = append(txIDs, tx.ID)
}
summary := bytes.Join(txIDs, []byte{})
return summary
}
func (b *Block) SetHash() {
information := bytes.Join([][]byte{utils.Int64ToByte(b.Timestamp),
b.PrevHash, b.Target, utils.Int64ToByte(b.Nonce), b.BackTXSummary()}, []byte{})
hash := sha256.Sum256(information)
b.Hash = hash[:]
}
修改CreateBlock与GenesisBlock两个函数
func CreateBlock(prevhash []byte, txs []*transaction.Transaction) *Block {
block := Block{time.Now().Unix(), []byte{},
prevhash, 0, []byte{}, txs}
block.Target = block.GetTarget()
block.Nonce = block.FindNonce()
block.SetHash() //所有数据添加好后再计算hash
return &block
}
func GenesisBlock() *Block {
tx := transaction.BaseTx([]byte("Arno"))
return CreateBlock([]byte{}, []*transaction.Transaction{tx})
}
这里我把1000个coin寄到了Arno哈希后的地址上
接下来我们转到proofofwork.go修改GetDataBaseNonce方法,让交易取代data
func (b *Block) GetDataBaseNonce(nonce int64) []byte {
data := bytes.Join([][]byte{
utils.Int64ToByte(b.Timestamp),
b.PrevHash,
utils.Int64ToByte(nonce),
b.Target,
b.BackTXSummary(),
},
[]byte{},
)
return data
}
接下来去blockchain.go修改AddBlock与CreateBlockChain两个函数
func (bc *BlockChain) AddBlock(txs []*transaction.Transaction) {
newBlock := CreateBlock(bc.Blocks[len(bc.Blocks)-1].Hash, txs)
bc.Blocks = append(bc.Blocks, newBlock)
}
// CreateBlockChain 初始化区块链
func CreateBlockChain() *BlockChain {
myBlockchain := BlockChain{}
myBlockchain.Blocks = append(myBlockchain.Blocks, GenesisBlock())
return &myBlockchain
}
接下来便是本章的大头:创建交易部分,首先找到足够的可用的coin,然后确定输入输出,最后创建交易,我们通过代码慢慢看。
创建交易信息
首先我们需要创建一个函数,该函数的功能是在区块链中查找指定地址的未花费交易。具体来说,它首先创建一个空的未花费交易列表unSpentTxs和一个空的已花费交易字典spentTxs,然后从最新的区块开始向前遍历区块链,对于每个区块中的每个交易,它会检查每个输出是否属于指定地址,如果是,则将该交易添加到未花费交易列表中。同时,它还会检查每个输入是否属于指定地址,并将该交易标记为已花费。
func (bc *BlockChain) FindUnspentTransactions(address []byte) []transaction.Transaction {
var unSpentTxs []transaction.Transaction
spentTxs := make(map[string][]int)
//unSpentTxs就是我们要返回包含指定地址的可用交易信息的切片
//spentTxs用于记录遍历区块链时那些已经被使用的交易信息的Output
//key值为交易信息的ID值(需要转成string)
//value值为Output在该交易信息中的序号
for idx := len(bc.Blocks) - 1; idx >= 0; idx-- {
block := bc.Blocks[idx]
for _, tx := range block.Transactions {
txID := hex.EncodeToString(tx.ID)
//从最后一个区块开始向前遍历区块链,然后遍历每一个区块中的交易信息
IterOutputs:
for outIdx, out := range tx.Outputs {
if spentTxs[txID] != nil {
for _, spentOut := range spentTxs[txID] {
if spentOut == outIdx {
continue IterOutputs
}
}
}
//遍历交易信息的Output,如果该Output在spentTxs中就跳过,说明该Output已被消费。
if out.ToAddressRight(address) {
unSpentTxs = append(unSpentTxs, *tx)
}
}
//否则确认ToAddress正确与否,正确就是我们要找的可用交易信息。
if !tx.IsBase() {
for _, in := range tx.Inputs {
if in.FromAddressRight(address) {
inTxID := hex.EncodeToString(in.TxID)
spentTxs[inTxID] = append(spentTxs[inTxID], in.OutIdx)
}
}
}
//检查当前交易信息是否为Base Transaction(主要是它没有input)
//如果不是就检查当前交易信息的input中是否包含目标地址
//有的话就将指向的Output信息加入到spentTxs中
}
}
return unSpentTxs
}
注释很好的讲解了每段代码的含义,下面是该函数具体实现方法:
- unSpentTxs 是一个用于存储未花费交易的切片。
- spentTxs 是一个 map,其中的键是交易 ID,值是一个包含已经花费的输出索引的整数数组。该 map 用于跟踪已经花费的交易,避免重复使用同一笔交易的输出。
- 对于每个区块,遍历其中的所有交易。
- 对于每个交易,检查它的每个输出是否是指定地址的输出。如果是,则将该交易添加到 unSpentTxs 数组中。
- 对于每个交易的输入,检查它是否是从指定地址发送的。如果是,则将该交易的输出索引添加到 spentTxs map中,表示这个输出已经被花费过了。
- 如果在 spentTxs map 中已经存在与当前交易 ID相同的键,那么遍历这个交易的所有输出,检查每个输出是否已经被花费。如果已经被花费,则继续遍历下一个输出;如果未被花费,则将该交易添加到unSpentTxs 数组中,并继续遍历下一个输出。
- 最后返回 unSpentTxs 数组,其中包含了所有未花费的交易。
利用FindUnspentTransactions函数,我们可以找到一个地址的所有UTXO以及该地址对应的资产总和。
func (bc *BlockChain) FindUTXOs(address []byte) (int, map[string]int) {
unspentOuts := 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.Outputs {
if out.ToAddressRight(address) {
accumulated += out.Value
unspentOuts[txID] = outIdx
continue Work
}
}
}
return accumulated, unspentOuts
}
注意:函数中使用了continue Work语句,这意味着一旦找到了属于给定地址的未花费输出,就会跳过当前交易的剩余输出,因为一笔交易只能有一个输出与该地址相关联。
Work 是一个标签(label),被用在这个代码块之前的 for 循环上,它的作用是为这个代码块创建一个标识符,以便在内部跳出这个代码块时使用。
在这个函数中,如果找到一个未使用的输出,就会累加其价值到 accumulated 变量中,同时把这个输出的信息添加到 unspentOuts 映射中,标识该输出的所在交易 ID 和输出编号。
因为一笔交易只能有一个输出被用于支付地址,所以在找到符合条件的输出后,需要跳出当前的循环。这里使用 continue Work 语句来跳过当前迭代并开始下一次迭代。如果没有找到符合条件的输出,就继续执行下一次循环。
当然,我们在实际应用中不需要每次都要找到所有UTXO,我们只需找到资产总量大于本次交易转账额的一部分UTXO就行。代码如下
func (bc *BlockChain) FindSpendableOutputs(address []byte, amount int) (int, map[string]int) {
unspentOuts := 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.Outputs {
if out.ToAddressRight(address) && accumulated < amount {
accumulated += out.Value
unspentOuts[txID] = outIdx
if accumulated >= amount {
break Work
}
continue Work
}
}
}
return accumulated, unspentOuts
}
现在我们可以来构造CreateTransaction函数了。
func (bc *BlockChain) CreateTransaction(from, to []byte, amount int) (*transaction.Transaction, bool) {
var inputs []transaction.TxInput
var outputs []transaction.TxOutput
acc, validOutputs := bc.FindSpendableOutputs(from, amount)
if acc < amount {
fmt.Println("Not enough coins!")
return &transaction.Transaction{}, false
}
for txid, outidx := range validOutputs {
txID, err := hex.DecodeString(txid)
utils.Handle(err)
input := transaction.TxInput{txID, outidx, from}
inputs = append(inputs, input)
}
outputs = append(outputs, transaction.TxOutput{amount, to})
if acc > amount {
outputs = append(outputs, transaction.TxOutput{acc - amount, from})
}
tx := transaction.Transaction{nil, inputs, outputs}
tx.SetID()
return &tx, true
}
这段代码实现了在区块链中创建交易的功能。其输入参数包括发件人的地址 from,收件人的地址 to,以及转账的数量 amount。该函数会先通过调用 FindSpendableOutputs 函数找到发件人地址上有多少金额,然后判断这个数量是否足够转账,如果不足够则返回一个空的交易和一个标志表示转账是否成功。如果足够,则构造交易的输入和输出,其中输入包括之前的交易ID和输出索引,输出则包括给收件人的转账和可能找零的部分给发件人。最后对交易进行哈希处理并返回交易指针和转账是否成功的标志。
为了模拟真实挖矿场景,我们现在不希望再使用AddBlock直接添加区块进入到区块链中,而是预留一个函数模拟一下从交易信息池中获取交易信息打包并挖矿这个过程
func (bc *BlockChain) Mine(txs []*transaction.Transaction) {
bc.AddBlock(txs)
}
调试
我们终于完成了交易的构建,下面可以开始调试了
package main
import (
"fmt"
"lighteningchain/blockchain"
"lighteningchain/transaction"
)
func main() {
txPool := make([]*transaction.Transaction, 0)
var tempTx *transaction.Transaction
var ok bool
var property int
chain := blockchain.CreateBlockChain()
property, _ = chain.FindUTXOs([]byte("Arno"))
fmt.Println("Balance of Arno: ", property)
tempTx, ok = chain.CreateTransaction([]byte("Arno"), []byte("Bravo"), 100)
if ok {
txPool = append(txPool, tempTx)
}
chain.Mine(txPool)
txPool = make([]*transaction.Transaction, 0)
property, _ = chain.FindUTXOs([]byte("Arno"))
fmt.Println("Balance of Arno: ", property)
tempTx, ok = chain.CreateTransaction([]byte("Bravo"), []byte("Charlie"), 200) // this transaction is invalid
if ok {
txPool = append(txPool, tempTx)
}
tempTx, ok = chain.CreateTransaction([]byte("Bravo"), []byte("Charlie"), 50)
if ok {
txPool = append(txPool, tempTx)
}
tempTx, ok = chain.CreateTransaction([]byte("Arno"), []byte("Charlie"), 100)
if ok {
txPool = append(txPool, tempTx)
}
chain.Mine(txPool)
txPool = make([]*transaction.Transaction, 0)
property, _ = chain.FindUTXOs([]byte("Arno"))
fmt.Println("Balance of Arno: ", property)
property, _ = chain.FindUTXOs([]byte("Bravo"))
fmt.Println("Balance of Bravo: ", property)
property, _ = chain.FindUTXOs([]byte("Charlie"))
fmt.Println("Balance of Charlie: ", property)
for _, block := range chain.Blocks {
fmt.Printf("Timestamp: %d\n", block.Timestamp)
fmt.Printf("hash: %x\n", block.Hash)
fmt.Printf("Previous hash: %x\n", block.PrevHash)
fmt.Printf("nonce: %d\n", block.Nonce)
fmt.Println("Proof of Work validation:", block.ValidatePoW())
}
//I want to show the bug at this version.
tempTx, ok = chain.CreateTransaction([]byte("Bravo"), []byte("Charlie"), 30)
if ok {
txPool = append(txPool, tempTx)
}
tempTx, ok = chain.CreateTransaction([]byte("Bravo"), []byte("Arno"), 30)
if ok {
txPool = append(txPool, tempTx)
}
chain.Mine(txPool)
txPool = make([]*transaction.Transaction, 0)
for _, block := range chain.Blocks {
fmt.Printf("Timestamp: %d\n", block.Timestamp)
fmt.Printf("hash: %x\n", block.Hash)
fmt.Printf("Previous hash: %x\n", block.PrevHash)
fmt.Printf("nonce: %d\n", block.Nonce)
fmt.Println("Proof of Work validation:", block.ValidatePoW())
}
property, _ = chain.FindUTXOs([]byte("Arno"))
fmt.Println("Balance of Arno: ", property)
property, _ = chain.FindUTXOs([]byte("Bravo"))
fmt.Println("Balance of Bravo: ", property)
property, _ = chain.FindUTXOs([]byte("Charlie"))
fmt.Println("Balance of Charlie: ", property)
}
输出如下:
Balance of Arno: 1000
Balance of Arno: 900
Not enough coins!
Balance of Arno: 800
Balance of Bravo: 50
Balance of Charlie: 150
Timestamp: 1677745707
hash: d09a7ad28701e3597c1a952d79cf85bea6fc0d7c9d9f0efeab9a3b79d42ae68d
Previous hash:
nonce: 4534
Proof of Work validation: true
Timestamp: 1677745707
hash: 8a1583944b8389101fc181377079d1c69c92019a926b50fe120aa25bf6c563a9
Previous hash: d09a7ad28701e3597c1a952d79cf85bea6fc0d7c9d9f0efeab9a3b79d42ae68d
nonce: 23955
Proof of Work validation: true
Balance of Arno: 830
Balance of Bravo: 40766ec14b1ce1c72297ee55a023a650ffd078df238080cdb54
Balance of Charlie: 1808d45324a568bc194bc8fcaa6b995eadcf6b2cf5d2f6ee381fa04a7b
nonce: 19545
Proof of Work validation: true
Process finished with the exit code 0
hash: d09a7ad28701e3597c1a952d79cf85bea6fc0d7c9d9f0efeab9a3b79d42ae68d
Previous hash:
nonce: 4534
Proof of Work validation: true
Timestamp: 1677745707
hash: 8a1583944b8389101fc181377079d1c69c92019a926b50fe120aa25bf6c563a9
Previous hash: d09a7ad28701e3597c1a952d79cf85bea6fc0d7c9d9f0efeab9a3b79d42ae68d
仔细看,从“Bravo”到“Charlie”金额为200,可是他只有100块,这是无法完成的,但是为什么成功了呢?这是因为上一个块的输出未被验证,所以它可以被使用两次,这样就有可能出现区块链的错误,也就是我们熟知的“双花问题”。
总结
这一章到此终于结束了,本章我们理解了交易信息以及其UTXO模型。你可能已经注意到我们的区块链系统在现阶段不能保存,每一次运行都需要重新创建区块链,我们将在下章学习如何保存我们的区块链,并建立一个Command Line程序管理我们的区块链系统