用go编写区块链系列之4--交易1

0 介绍

比特币区块链的核心就是交易,区块链唯一的目的就是用一种安全可信的方式去存储交易,交易一经创建就无法更改。这章中我们将在区块链中引入交易。

1 比特币中的交易

如果你是开发网络应用的程序员,若让你开发一个在线支付交易,你多半会在数据库中创建俩张表:账户表和交易表。账户表中将会存储用户账户信息,比如个人信息和余额。交易表中将会存储交易信息,比如钱从一个账户转账给另一个账户。但是在比特币系统中,交易是以一种完全不同的方式实现的,在比特币中:

  • 没有账户
  • 没有余额
  • 没有地址
  • 没有货币
  • 没有发送者和接受者

由于区块链是一个公共公开的数据库,我们不想在其中存放敏感的钱包信息。账户中不保存资金。交易并不从一个地址发送到另一个地址。同时也不保存用户账户余额。只有交易。那么交易里面有什么呢?

  • 比特币交易

一个交易时输入和输出的集合:

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

ID是交易的hash;Vin是输入数组;Vout是输出数组。一笔新交易的输入时此前交易的输出(有一种例外情况,下文会讲到)。输出是存储钱币的地方。下图显示了交易的输入输出连接关系:

注意点:

(1)输出不一定会连接到输入。

(2)一笔交易中,输入可以引用多笔交易的输出。

(3)一个输入必须引用一个输出。

这篇文章中,我们使用钱、货币、支付、转账、账户等术语,但是在真实比特币系统中不存在这些概念。交易只是用脚本锁定一些值,这些值只能由锁定者才能解锁。

  • 交易输出

交易输出结构体TXOutput:

type TXOutput struct {
	Value        int
	ScriptPubKey string
}

TXOuntput通过Value值来"保存货币"。这里的“保存”就是通过ScriptPublikey密语来锁定。比特币使用Script脚本语言来定义锁定和解锁逻辑。我们在这里使用简单字符串形式的地址来实现锁定解锁逻辑。

需要注意的是交易输出是不可拆分的,你不能只花费它的一部分,而必须全部花出去。当这个交易输出的金额大于所需支付的金额时,将会产生一个找零返回给发送者。这很像现实世界中的钞票,你用10块钱去买一瓶3块钱的饮料,最后店家会给你找零7块钱。

  • 交易输入

定义输入结构体

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

交易输入都是引入以前交易的输出。其中Txid是引用的交易Hash,Vout是这个输出所在数组的索引。ScriptSig对应于交易输入中的ScriptPubKey,用来进行解锁操作。如果ScriptSig能够解锁,这笔输出就能够被用户使用。如果解锁失败,则这笔钱不能被用户使用。这个机制保证了用户不能使用属于别人的钱。我们在这里使用简单的字符串来进行解锁。关于公钥和签名,以后文章会讲到。

总结一下,交易输出用来保存钱币。每个输出都带有一个脚本密语,用来进行解锁操作。每笔交易必须带有至少一个输入和一个输出。一个输入必须引用自一笔以前的交易。一个输入带有一段锁定密语,用户必须有相应的钥匙解锁才能解锁该输入,解锁以后就能使用这笔输入中的钱。

这里产生了一个问题:输入和输出谁先产生?

  • 鸡生蛋还是蛋生鸡

输入引用输出,输入输出谁先谁后的问题是一个经典的鸡生蛋还是蛋生鸡的问题。但是在比特币中,输出先于输入产生。

当一个矿工开始挖掘一个区块的时候,它将一个coinbase交易添加到区块里面。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
}

coinbase交易只有一个交易输入,在这里设置它的Txid为空,设置Vout为-1,脚本ScriptSig设置为任意字符串。在比特币中,这个脚本通常设置为 “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks” 。subsidy是区块奖励,这里设置为10。

2 在区块链中存储交易

我们需要在区块中存储交易数组,所以我们改造Block结构体:

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

NewBlock函数和NewGenesisBlock函数也要更改:

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

	block.Hash = hash[:]
	block.Nonce = nonce

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

还要修改创建区块链的方法:

// CreateBlockchain creates a new blockchain DB
func CreateBlockchain(address string) *Blockchain {
	if dbExists() {
		fmt.Println("Blockchain already exists.")
		os.Exit(1)
	}

	var tip []byte
	db, err := bolt.Open(dbFile, 0600, nil)
	if err != nil {
		log.Panic(err)
	}

	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())
		if err != nil {
			log.Panic(err)
		}

		err = b.Put([]byte("l"), genesis.Hash)
		if err != nil {
			log.Panic(err)
		}
		tip = genesis.Hash

		return nil
	})

	if err != nil {
		log.Panic(err)
	}

	bc := Blockchain{tip, db}

	return &bc
}

3 PoW

PoW算法必须考虑引入交易。为了保证区块链对交易一致性的要求,我们修改ProofOfWork.prepareData方法:

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
}

其中的计算交易数组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[:]
}

我们在这里将交易数组的每个交易hash进行拼接然后再一起计算hash。在比特币中,使用交易的默克尔树的根哈希来表示交易数组,这种方法可以只需要根哈希就可以很快的查询到区块中是否存在某个交易,而不用下载所有的交易。

4 未花费交易输出(UTXO)

我们需要找出所有未花费出去的交易输出(UTXO),未花费意味着这些交易输出还没有被任何交易输入引用。在上图中,有这些UTXO:

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

当我们检查地址余额的时候,我们只需要我们可以解锁的交易。我们定义锁定和解锁函数:

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

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
	return in.ScriptSig == 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)

		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
}

首先往前遍历每一个区块,对于每一个区块,又遍历它的每一笔交易。下面代码:

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

检查交易输入时这里先排除coinbase交易,因为没有花费的coinbase交易输出通过上一步已经添加到unspentTXs数组中了。在这里我们只检查普通交易。遍历交易每一个输入,若该输入能够被用户地址解锁,我们就将该交易添加到已花费Map中。

下面是查找跟地址相关的UTXO的方法,我们先查找到所有未花费交易,然后只添加地址能够解锁的交易。

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

一个地址的余额是跟地址相关的所有未花费交易的钱币之和。

5 发送钱币

我们实现从一个地址from向另一个地址to发送amount金额的发送交易函数:

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
}

在创建新的交易输出的之前,我们需要找到所有可用的未花费输出,还要保证它们携带有足够的钱币,这是通过方法FindSpendableOutputs 实现的。然后,对于每一个UTXO输出,创建了一个引用它的交易输入。然后我们创建了俩个输出:

1 用接收者地址进行锁定的交易输出,它携带有不少于需要支付的金额。

2 用发送者地址进行锁定的交易输出,它是找零给发送者的钱币。

FindSpendableOutputs 方法:

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
}

这个方法遍历所有可用的为花费交易输出,然后收集他们的余额,若金额不小于需要支付的金额amount时,我们就退出,这保证了我们收集到足够的金额又不至于过多。

然后我们实现区块挖掘函数:

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

实现交易发送命令:

func (cli *CLI) send(from, to string, amount int) {
	bc := NewBlockchain()
	defer bc.db.Close()

	tx := NewUTXOTransaction(from, to, amount, bc)
	bc.MineBlock([]*Transaction{tx})
	fmt.Println("Success!")
}

发送金币也就是也就是创建一笔交易并且通过挖矿将它添加到区块中。比特币没有立即挖矿,而是先将所有的新交易添加到内存中的交易池,当一个矿工要开始挖掘区块时,它将交易池中的所有交易打包到新区快。当区块被挖掘出来并发布到区块链中后,这些交易也将生效。

6 验证

所有源码见https://github.com/Jeiwan/blockchain_go/blob/part_4

运行工程,创建区块链:

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe createblockcha in -address Ivan
000024a0c2b4c7cf9c44e065ae2d4a8f4e6d2ea4c1ea490e599cce7c1909c384
21247
Done!

查询Ivan余额:

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Ivan
Balance of 'Ivan': 10

发送一笔交易并查询余额:

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe send -from Ivan -to Peter -amount 6
00000e146f87f072fc5677dd9e88bf61e3f2f34bd672472e09c64c80676aef5b
50939
Success!

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Ivan
Balance of 'Ivan': 4

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Peter
Balance of 'Peter': 6

发送多笔交易并查询余额:

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe send -from Peter -to Helen -amount 2
0000d0a4d4c468ffcc1112c9395ab73e2f4abfa1b88448f4f8a255ad359bba5e
7289
Success!

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe send -from Ivan -to Helen -amount 2
0000c6c86094e1201dcbe9a913772745a9930fe772302aa634dcc212d3e02616
17825
Success!

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe send -from Helen -to Cookie -amount 3
00005b12a0726a68188b7e0148c26d69740f755d8eade14c53fef294d98eb5e8
92174
Success!

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Ivan
Balance of 'Ivan': 2

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Peter
Balance of 'Peter': 4

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Helen
Balance of 'Helen': 1

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Cookie
Balance of 'Cookie': 3

7 结论

我们已经实现了一个带交易的区块链,但是还缺少比特币区块链的一些关键特性:

1 地址。我们还没有基于私钥的地址。

2 挖矿奖励。现在还没有挖矿奖励。

3 UTXO集合。查询余额需要遍历整个区块链,当区块数很多时这将会是很耗时的操作。UTXO集合将能加快整个过程。

4 交易池。交易在被打包到新区快前将临时保存在交易池中。在现在的系统中一个区块只包含一笔交易,这样很低效。

猜你喜欢

转载自blog.csdn.net/liuzhijun301/article/details/82773262
今日推荐