golang重写区块链——0.4 链上交易(无地址版本)

区块链的作用就是要实现交易,一种无法篡改永久保存的交易。比特币区块链还没有像以太坊一样已经实现了账户的概念,比特币区块链上的交易双方是地址,地址背后才是人,人和地址不是一一对应的关系,一个人可以拥有很多比特币的地址。

          ——注:底端有完整的代码,代码里面有详细的注解,可以直接看完整的代码,github端我随后会上传

    在一笔交易中,是由输入和输出来形成的,首先我们重新创建单独的transaction包。在包里我们构建了三个数据结构和挖矿输出交易,代码如下:

package transaction

import (
	"crypto/sha256"
	"encoding/gob"
	"bytes"
	"fmt"
	"log"
)

const subsidy = 50  //挖矿奖励
/*创建一个交易的数据结构,交易是由交易ID、交易输入、交易输出组成的,
一个交易有多个输入和多个输出,所以这里的交易输入和输出应该是切片类型的
*/
type Transaction struct {
	ID		[]byte
	Vin		[]TXInput
	Vout	        []TXOutput
}

/*
1、每一笔交易的输入都会引用之前交易的一笔或多笔交易输出
2、交易输出保存了输出的值和锁定该输出的信息
3、交易输入保存了引用之前交易输出的交易ID、具体到引用
该交易的第几个输出、能正确解锁引用输出的签名信息
*/
//交易输出
type TXOutput struct {
	Value			int	//输出的值(可以理解为金额)
	ScriptPubKey	string	// 锁定该输出的脚本(目前还没实现地址,所以还不能锁定该输出为具体哪个地址所有)
}
//交易输入
type TXInput struct {
	Txid 	[]byte //引用的之前交易的ID
	Vout	int 	//引用之前交易输出的具体是哪个输出(一个交易中输出一般有很多)
	ScriptSig	string  // 能解锁引用输出交易的签名脚本(目前还没实现地址,所以本章不能实现此功能)
}

/*
	区块链上存储的交易都是由这些输入输出交易所组成的,
一个输入交易必须引用之前的输出交易,一个输出交易会被之后的输入所引用。
    问题来了,在最开始的区块链上是先有输入还是先有输出喃?
答案是先有输出,因为是区块链的创世区块产生了第一个输出,
这个输出也就是我们常说的挖矿奖励-狗头金,每一个区块都会有一个这样的输出,
这是奖励给矿工的交易输出,这个输出是凭空产生的。
*/
//现在我们来创建一个这样的coinbase挖矿输出
//to 代表此输出奖励给谁,一般都是矿工地址,data是交易附带的信息
func NewCoinbaseTX(to,data string) *Transaction {
	if data == "" {
		data = fmt.Sprintf("奖励给 '%s'",to)
	}
	//此交易中的交易输入,没有交易输入信息
	txin := TXInput{[]byte{},-1,data}
	//交易输出,subsidy为奖励矿工的币的数量
	txout := TXOutput{subsidy,to}
	//组成交易
	tx := Transaction{nil,[]TXInput{txin},[]TXOutput{txout}}
	//设置该交易的ID
	tx.SetID()
	return &tx
}
//设置交易ID,交易ID是序列化tx后再哈希
func (tx *Transaction) SetID() {
	var hash [32]byte
	var encoder bytes.Buffer

	enc := gob.NewEncoder(&encoder)
	err := enc.Encode(tx)
	if err != nil {
		log.Panic(err)
	}
	hash = sha256.Sum256(encoder.Bytes())
	tx.ID =  hash[:]
}

/*
1、每一个区块至少存储一笔coinbase交易,所以我们在区块的字段中把Data字段换成交易。
2、把所有涉及之前Data字段都要换了,比如NewBlock()、GenesisBlock()、pow里的函数
*/

    现在我们的链有了交易的概念,我们要把区块中的字段Data换成我们的交易transactions字段,然后把要加入的区块都要有coinbaseTX交易。把所有涉及到的Data字段都需要修改为tx交易字段,让我们去仔细修改吧。

在blockchain包里的创世区块的生成和创建新区块链的函数修改如下:

//创建创世区块  /修改/
func NewGenesisBlock(coinbase *transaction.Transaction) *block.Block {
	return pow.NewBlock([]*transaction.Transaction{coinbase},[]byte{})
}
//实例化一个区块链,默认存储了创世区块 ,接收一个地址为挖矿奖励地址 /修改/
func NewBlockchain(address string) *Blockchain {
	//return &Blockchain{[]*block.Block{NewGenesisBlock()}}
	var tip []byte
	//打开一个数据库文件,如果文件不存在则创建该名字的文件
	db,err := bolt.Open(dbFile,0600,nil)
	if err != nil {
		log.Panic(err)
	}
	//读写操作数据库
	err = db.Update(func(tx *bolt.Tx) error{
		b := tx.Bucket([]byte(blocksBucket))
		//查看名字为blocksBucket的Bucket是否存在
		if b == nil {
			//不存在则从头 创建
			fmt.Println("不存在区块链,需要重新创建一个区块链...")
			coinbaseData := "我是zyj0813,我创建了此链..."
			//genesis := NewGenesisBlock()	//创建创世区块
			//此时的创世区块就要包含交易coinbaseTx
			cbtx := transaction.NewCoinbaseTX(address,coinbaseData)
			genesis := NewGenesisBlock(cbtx)

			b,err := tx.CreateBucket([]byte(blocksBucket)) //创建名为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) //"l"键对应区块链顶端区块的哈希
			if err != nil {
				log.Panic(err)
			}
			tip = genesis.Hash //指向最后一个区块,这里也就是创世区块
		} else {
			//如果存在blocksBucket桶,也就是存在区块链
			//通过键"l"映射出顶端区块的Hash值
			tip = b.Get([]byte("l"))
		}

		return nil
	})

	bc := Blockchain{tip,db}  //此时Blockchain结构体字段已经变成这样了
	return &bc
}

    AddBlock函数修改如下:

//把区块添加进区块链
func (bc *Blockchain) AddBlock(transactions []*transaction.Transaction) {
	var lastHash []byte
	//只读的方式浏览数据库,获取当前区块链顶端区块的哈希,为加入下一区块做准备
	err := bc.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		lastHash = b.Get([]byte("l"))	//通过键"l"拿到区块链顶端区块哈希

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

	//prevBlock := bc.Blocks[len(bc.Blocks)-1]
	//求出新区块
	newBlock := pow.NewBlock(transactions,lastHash)
	// bc.Blocks = append(bc.Blocks,newBlock)
	//把新区块加入到数据库区块链中
	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)
		bc.tip = newBlock.Hash

		return nil
	})
}

在pow包中要修改一下prepareData(nonce int)函数和NewBlock()函数:

//准备需要进行哈希的数据
func (pow *ProofOfWork) prepareData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.block.PrevBlockHash,
			pow.block.HashTransactions(),   //这里被修改,把之前的Data字段修改成交易字段的哈希
			[]byte(strconv.FormatInt(pow.block.Timestamp,10)),
			[]byte(strconv.FormatInt(targetBits,10)),
			[]byte(strconv.FormatInt(int64(nonce),10)),
		},
		[]byte{},
	)
	return data
}
//实例化一个区块    /更改data为transaction/
func NewBlock(transactions	[]*transaction.Transaction,prevBlockHash []byte) *block.Block {
	block := &block.Block{time.Now().Unix(),transactions,prevBlockHash,[]byte{},0}
	// block.SetHash()

	pow := NewProofOfWork(block)
	nonce,hash := pow.Run()
	block.Hash = hash
	block.Nonce = nonce
	return block
}

    在CLI包中我们这时不忙修改,到最后会有大的改动,因为后面我们涉及到查询余额和发送币交易。下面我们将实现查询余额,首先查询余额我们要找到链上的未花费交易输出,换句话讲,就是要找到那些之前输出的交易没有被后面输入交易所引用的输出,就叫做未花费交易输出(UTXO),当然我们查询余额往往不能查询链上所有地址的余额,我们只能查询我们自己所拥有地址的余额,换句话说我们所查询的未花费输出的余额都是我们自己能解锁的那部分余额。目前我们还没有涉及到秘钥,所以我们只能使用用户定义的地址address来代替锁定和解锁的秘钥。首先我们需要在transaction包里面 定义输入和输出交易上的锁定和解锁方法:


//定义在输入和输出上的锁定和解锁方法,目的是让用户只能花自己所用于地址上的币
//输入上锁定的秘钥,表示能引用的输出是unlockingData
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
	return in.ScriptSig == unlockingData
}
//输出上的解锁秘钥,表示能被引用的输入是unlockingData
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
	return out.ScriptPubKey == unlockingData 
}

在这里我们只是简单的用输入输出的脚本字段与unlocking进行了比较,后续章节我们实现了私钥和地址之后再进行改进。

    下面我们就要进行未花费交易输出的交易的查找了,这是本章的重点:

    我们查找各个区块中的交易应该是在blockchain包里面进行查找,因为区块都在链上,所以,我们在blockchain包里构建查找函数,代码如下:

找到当前用户的地址下的未花费输出的所有交易:

//在区块链上找到每一个区块中属于address用户的未花费交易输出,返回未花费输出的交易切片
func (bc *Blockchain) FindUnspentTransactions(address string) []transaction.Transaction {
	var unspentTXs []transaction.Transaction
	//创建一个map,存储已经花费了的交易输出
	spentTXOs := make(map[string][]int)
	//因为要在链上遍历区块,所以要使用到迭代器
	bci := bc.Iterator()

	for {
		block := bci.Next()  //迭代

		//遍历当前区块上的交易
		for _,tx := range block.Transactions {
			txID := hex.EncodeToString(tx.ID) //把交易ID转换成string类型,方便存入map中
		
		//标签
		Outputs:
		//遍历当前交易中的输出切片,取出交易输出
			for outIdx,out := range tx.Vout {
				//在已经花费了的交易输出map中,如果没有找到对应的交易输出,则表示当前交易的输出未花费
				//反之如下
				if spentTXOs[txID] != nil {
					//存在当前交易的输出中有已经花费的交易输出,
					//则我们遍历map中保存的该交易ID对应的输出的index 
					//提示:(这里的已经花费的交易输出index其实就是输入TXInput结构体中的Vout字段)
					for _,spentOut := range spentTXOs[txID] {
						//首先要清楚当前交易输出是一个切片,里面有很多输出,
						//如果map里存储的引用的输出和我们当前遍历到的输出index重合,则表示该输出被引用了
						if spentOut == outIdx {
							continue Outputs  //我们就继续遍历下一轮,找到未被引用的输出
						}
					}
				}
				//到这里是得到此交易输出切片中未被引用的输出

				//这里就要从这些未被引用的输出中筛选出属于该用户address地址的输出
				if out.CanBeUnlockedWith(address) {
					unspentTXs = append(unspentTXs,*tx)
				}
			}
			//判断是否为coinbase交易
			if tx.IsCoinbase() == false { 		
				//如果不是,则遍历当前交易的输入
				for _,in := range tx.Vin {
					//如果当前交易的输入是被该地址address所花费的,
					//则在map上记录该输入引用的该地址对应的交易输出
					if in.CanUnlockOutputWith(address) {
						inTxID := hex.EncodeToString(in.Txid)
						spentTXOs[inTxID] = append(spentTXOs[inTxID],in.Vout)
					}
				}
			}
		}
		//退出for循环的条件就是遍历到的创世区块后
		if len(block.PrevBlockHash) == 0 {
			break
		}
	}
	return unspentTXs
}

    要求出余额,则需要得到未花费交易输出,因为只有在交易输出这个结构体里面才有value字段,才能求出所有的未花费交易输出的value字段值的总和就是账户余额了。首先求得未花费交易输出:

//通过找到未花费输出交易的集合,我们返回集合中的所有未花费的交易输出
func (bc *Blockchain) FindUTXO(address string) []transaction.TXOutput {
	var UTXOs []transaction.TXOutput
	//找到address地址下的未花费交易输出的交易的集合
	unspentTransactions := bc.FindUnspentTransactions(address)
	//遍历交易集合得到交易,从交易中提取出输出字段Vout,从输出字段中提取出属于address的输出
	for _,tx := range unspentTransactions {
		for _, out := range tx.Vout {
			if out.CanBeUnlockedWith(address) {
				UTXOs = append(UTXOs,out)
			}
		}
	}
	//返回未花费交易输出
	return UTXOs 
}

    现在来实现余额getBalance函数,该函数在CLI包中实现,因为我们需要在命令行接口中直接调用求余额这个命令:

//求账户余额
func (cli *CLI) getBalance(address string) {
	bc := blockchain.NewBlockchain(address)
	defer bc.Db().Close()

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

	//遍历UTXOs中的交易输出out,得到输出字段out.Value,求出余额
	for _,out := range UTXOs {
		balance += out.Value
	}

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

在上面的内容中,我们实现的交易只是coinbase的交易,现在我们要实现的是正真意义上的把币转给其他地址的交易。我们现在还没有交易池的概念,所以创建一笔交易就需要挖出一个区块,一个区块值能存储一笔交易,发送币代码如下(还是在blockchain包里面进行):

//发送币操作,相当于创建一笔未花费输出交易
func NewUTXOTransaction(from,to string,amount int,bc *Blockchain) *transaction.Transaction {
	var inputs []transaction.TXInput
	var outputs []transaction.TXOutput
	//validOutputs是一个存放要用到的未花费输出的交易/输出的map 
	acc,validOutputs := bc.FindSpendableOutputs(from,amount)

	if acc < amount {
		log.Panic("ERROR:Not enough tokens...")
	}
	//通过validOutputs里面的数据来放入建立一个输入列表
	for txid,outs := range validOutputs {
		//反序列化得到txID
		txID,err := hex.DecodeString(txid)
		if err != nil {
			log.Panic(err)
		}
		//遍历输出outs切片,得到TXInput里的Vout字段值
		for _,out := range outs {
			input := transaction.TXInput{txID,out,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
}

//找到可以花费的交易输出,这是基于上面的FindUnspentTransactions 方法
func (bc *Blockchain) FindSpendableOutputs(address string,amount int) (int,map[string][]int) {
	//未花费交易输出map集合
	unspentOutputs := make(map[string][]int)
	//未花费交易
	unspentTXs := bc.FindUnspentTransactions(address)
	accumulated := 0	//累加未花费交易输出中的Value值

	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
}

send方法:

//send方法
func (cli *CLI) send(from,to string,amount int) {
	bc := blockchain.NewBlockchain(from)
	defer bc.Db().Close()

	tx := blockchain.NewUTXOTransaction(from,to,amount,bc)
	//挖出一个包含该交易的区块,此时区块只有这一个交易
	bc.MineBlock([]*transaction.Transaction{tx})
	fmt.Println("发送成功...")
}

由于代码改动有点大,尤其是CLI包里面代码几乎全动过,所以我把完整的代码附在下面:

block包:

package block

import (
	"crypto/sha256"
	"encoding/gob"
	"bytes"
	"log"
	"go_code/A_golang_blockchain/transaction"
)
//区块的结构体
type Block struct {
	Timestamp		int64
	Transactions	[]*transaction.Transaction
	PrevBlockHash	[]byte
	Hash 			[]byte
	Nonce			int
}

//区块交易字段的哈希,因为每个交易ID是序列化并哈希后的交易数据结构,所以我们只需把所以交易的ID进行哈希就可以了
func (b *Block) HashTransactions() []byte {
	var txHash [32]byte
	var txHashes [][]byte
	for _,tx := range b.Transactions {
		txHashes = append(txHashes,tx.ID)
	}
	txHash = sha256.Sum256(bytes.Join(txHashes,[]byte{}))

	return txHash[:]
}

//0.3 实现Block的序列化
func (b *Block) Serialize() []byte {
	//首先定义一个buffer存储序列化后的数据
	var result bytes.Buffer
	//实例化一个序列化实例,结果保存到result中
	encoder := gob.NewEncoder(&result)
	//对区块进行实例化
	err := encoder.Encode(b)
	if err != nil {
		log.Panic(err)
	}
	return result.Bytes()
}

//0.3 实现反序列化函数
func DeserializeBlock(d []byte) *Block {
	var block Block
	decoder := gob.NewDecoder(bytes.NewReader(d))
	err := decoder.Decode(&block)
	if err != nil {
		log.Panic(err)
	}
	return &block
}

blockchain包:

package blockchain

import (
	"encoding/hex"
	"fmt"
	"github.com/boltdb/bolt"
	"go_code/A_golang_blockchain/block"
	"go_code/A_golang_blockchain/pow"
	"go_code/A_golang_blockchain/transaction"
	"log"
)
/*
	区块链实现
*/
const dbFile = "blockchain.db"
const blocksBucket = "blocks"
const genesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
//区块链
type Blockchain struct {
	tip		[]byte
	db 		*bolt.DB
}

//工厂模式db
func(bc *Blockchain) Db() *bolt.DB {
	return bc.db
}

//把区块添加进区块链,挖矿
func (bc *Blockchain) MineBlock(transactions []*transaction.Transaction) {
	var lastHash []byte
	//只读的方式浏览数据库,获取当前区块链顶端区块的哈希,为加入下一区块做准备
	err := bc.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		lastHash = b.Get([]byte("l"))	//通过键"l"拿到区块链顶端区块哈希

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

	//prevBlock := bc.Blocks[len(bc.Blocks)-1]
	//求出新区块
	newBlock := pow.NewBlock(transactions,lastHash)
	// bc.Blocks = append(bc.Blocks,newBlock)
	//把新区块加入到数据库区块链中
	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)
		bc.tip = newBlock.Hash

		return nil
	})
}

//创建创世区块  /修改/
func NewGenesisBlock(coinbase *transaction.Transaction) *block.Block {
	return pow.NewBlock([]*transaction.Transaction{coinbase},[]byte{})
}
//实例化一个区块链,默认存储了创世区块 ,接收一个地址为挖矿奖励地址 /修改/
func NewBlockchain(address string) *Blockchain {
	//return &Blockchain{[]*block.Block{NewGenesisBlock()}}
	var tip []byte
	//打开一个数据库文件,如果文件不存在则创建该名字的文件
	db,err := bolt.Open(dbFile,0600,nil)
	if err != nil {
		log.Panic(err)
	}
	//读写操作数据库
	err = db.Update(func(tx *bolt.Tx) error{
		b := tx.Bucket([]byte(blocksBucket))
		//查看名字为blocksBucket的Bucket是否存在
		if b == nil {
			//不存在则从头 创建
			fmt.Println("不存在区块链,需要重新创建一个区块链...")
			
			//genesis := NewGenesisBlock()	//创建创世区块
			//此时的创世区块就要包含交易coinbaseTx
			cbtx := transaction.NewCoinbaseTX(address,genesisCoinbaseData)
			genesis := NewGenesisBlock(cbtx)

			b,err := tx.CreateBucket([]byte(blocksBucket)) //创建名为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) //"l"键对应区块链顶端区块的哈希
			if err != nil {
				log.Panic(err)
			}
			tip = genesis.Hash //指向最后一个区块,这里也就是创世区块
		} else {
			//如果存在blocksBucket桶,也就是存在区块链
			//通过键"l"映射出顶端区块的Hash值
			tip = b.Get([]byte("l"))
		}

		return nil
	})

	bc := Blockchain{tip,db}  //此时Blockchain结构体字段已经变成这样了
	return &bc
}

//分割线——————迭代器——————
type BlockchainIterator struct {
	currentHash 	[]byte
	db 				*bolt.DB
}
//当需要遍历当前区块链时,创建一个此区块链的迭代器
func (bc *Blockchain) Iterator() *BlockchainIterator {
	bci := &BlockchainIterator{bc.tip,bc.db}

	return bci
}

//迭代器的任务就是返回链中的下一个区块
func (i *BlockchainIterator) Next() *block.Block {
	var Block *block.Block

	//只读方式打开区块链数据库
	err := i.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		//获取数据库中当前区块哈希对应的被序列化后的区块
		encodeBlock := b.Get(i.currentHash)
		//反序列化,获得区块
		Block = block.DeserializeBlock(encodeBlock)

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

	//把迭代器中的当前区块哈希设置为上一区块的哈希,实现迭代的作用
	i.currentHash =Block.PrevBlockHash

	return Block

}

//在区块链上找到每一个区块中属于address用户的未花费交易输出,返回未花费输出的交易切片
func (bc *Blockchain) FindUnspentTransactions(address string) []transaction.Transaction {
	var unspentTXs []transaction.Transaction
	//创建一个map,存储已经花费了的交易输出
	spentTXOs := make(map[string][]int)
	//因为要在链上遍历区块,所以要使用到迭代器
	bci := bc.Iterator()

	for {
		block := bci.Next()  //迭代

		//遍历当前区块上的交易
		for _,tx := range block.Transactions {
			txID := hex.EncodeToString(tx.ID) //把交易ID转换成string类型,方便存入map中
		
		//标签
		Outputs:
		//遍历当前交易中的输出切片,取出交易输出
			for outIdx,out := range tx.Vout {
				//在已经花费了的交易输出map中,如果没有找到对应的交易输出,则表示当前交易的输出未花费
				//反之如下
				if spentTXOs[txID] != nil {
					//存在当前交易的输出中有已经花费的交易输出,
					//则我们遍历map中保存的该交易ID对应的输出的index 
					//提示:(这里的已经花费的交易输出index其实就是输入TXInput结构体中的Vout字段)
					for _,spentOut := range spentTXOs[txID] {
						//首先要清楚当前交易输出是一个切片,里面有很多输出,
						//如果map里存储的引用的输出和我们当前遍历到的输出index重合,则表示该输出被引用了
						if spentOut == outIdx {
							continue Outputs  //我们就继续遍历下一轮,找到未被引用的输出
						}
					}
				}
				//到这里是得到此交易输出切片中未被引用的输出

				//这里就要从这些未被引用的输出中筛选出属于该用户address地址的输出
				if out.CanBeUnlockedWith(address) {
					unspentTXs = append(unspentTXs,*tx)
				}
			}
			//判断是否为coinbase交易
			if tx.IsCoinbase() == false { 		
				//如果不是,则遍历当前交易的输入
				for _,in := range tx.Vin {
					//如果当前交易的输入是被该地址address所花费的,就会有对应的该地址的引用输出
					//则在map上记录该输入引用的该地址对应的交易输出
					if in.CanUnlockOutputWith(address) {
						inTxID := hex.EncodeToString(in.Txid)
						spentTXOs[inTxID] = append(spentTXOs[inTxID],in.Vout)
					}
				}
			}
		}
		//退出for循环的条件就是遍历到的创世区块后
		if len(block.PrevBlockHash) == 0 {
			break
		}
	}
	return unspentTXs
}

//通过找到未花费输出交易的集合,我们返回集合中的所有未花费的交易输出
func (bc *Blockchain) FindUTXO(address string) []transaction.TXOutput {
	var UTXOs []transaction.TXOutput
	//找到address地址下的未花费交易输出的交易的集合
	unspentTransactions := bc.FindUnspentTransactions(address)
	//遍历交易集合得到交易,从交易中提取出输出字段Vout,从输出字段中提取出属于address的输出
	for _,tx := range unspentTransactions {
		for _, out := range tx.Vout {
			if out.CanBeUnlockedWith(address) {
				UTXOs = append(UTXOs,out)
			}
		}
	}
	//返回未花费交易输出
	return UTXOs 
}

//发送币操作,相当于创建一笔未花费输出交易
func NewUTXOTransaction(from,to string,amount int,bc *Blockchain) *transaction.Transaction {
	var inputs []transaction.TXInput
	var outputs []transaction.TXOutput
	//validOutputs是一个存放要用到的未花费输出的交易/输出的map 
	acc,validOutputs := bc.FindSpendableOutputs(from,amount)

	if acc < amount {
		log.Panic("ERROR:Not enough tokens...")
	}
	//通过validOutputs里面的数据来放入建立一个输入列表
	for txid,outs := range validOutputs {
		//反序列化得到txID
		txID,err := hex.DecodeString(txid)
		if err != nil {
			log.Panic(err)
		}
		//遍历输出outs切片,得到TXInput里的Vout字段值
		for _,out := range outs {
			input := transaction.TXInput{txID,out,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
}

//找到可以花费的交易输出,这是基于上面的FindUnspentTransactions 方法
func (bc *Blockchain) FindSpendableOutputs(address string,amount int) (int,map[string][]int) {
	//未花费交易输出map集合
	unspentOutputs := make(map[string][]int)
	//未花费交易
	unspentTXs := bc.FindUnspentTransactions(address)
	accumulated := 0	//累加未花费交易输出中的Value值

	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
}

pow包:

package pow

import (
	"fmt"
	"crypto/sha256"
	"strconv"
	"bytes"
	"math/big"
	"go_code/A_golang_blockchain/block"
	"go_code/A_golang_blockchain/transaction"
	"math"
	"time"
)
//在实际的比特币区块链中,加入一个区块是非常困难的事情,其中运用得到的就是工作量证明

//创建一个工作量证明的结构体
type ProofOfWork struct {
	block *block.Block //要证明的区块
	target *big.Int //难度值
}
//声明一个挖矿难度
const targetBits = 10

//实例化一个工作量证明
func NewProofOfWork(b *block.Block) *ProofOfWork {
	target :=  big.NewInt(1)
	target.Lsh(target,uint(256 - targetBits))

	pow := &ProofOfWork{b,target}
	return pow
}

//准备需要进行哈希的数据
func (pow *ProofOfWork) prepareData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.block.PrevBlockHash,
			pow.block.HashTransactions(),   //这里被修改,把之前的Data字段修改成交易字段的哈希
			[]byte(strconv.FormatInt(pow.block.Timestamp,10)),
			[]byte(strconv.FormatInt(targetBits,10)),
			[]byte(strconv.FormatInt(int64(nonce),10)),
		},
		[]byte{},
	)
	return data
}

//进行工作量证明,证明成功会返回随机数和区块哈希
func (pow *ProofOfWork) Run() (int,[]byte) {
	nonce := 0
	var hash [32]byte
	var hashInt big.Int
	for nonce < math.MaxInt64 {
		data := pow.prepareData(nonce)
		hash = sha256.Sum256(data)
		hashInt.SetBytes(hash[:])

		//把哈希后的数据与难度值进行比较
		if hashInt.Cmp(pow.target) == -1 {
			fmt.Printf("工作量证明成功 hash= %x  nonce = %v\n",hash,nonce)
			break
		}else{
			nonce ++
		}
	}
	fmt.Println()

	return nonce,hash[:]
}

//实例化一个区块    /更改data为transaction/
func NewBlock(transactions	[]*transaction.Transaction,prevBlockHash []byte) *block.Block {
	block := &block.Block{time.Now().Unix(),transactions,prevBlockHash,[]byte{},0}
	// block.SetHash()

	pow := NewProofOfWork(block)
	nonce,hash := pow.Run()
	block.Hash = hash
	block.Nonce = nonce
	return block
}

//其他节点验证nonce是否正确
func (pow *ProofOfWork) Validate() bool {
	var hashInt big.Int

	data := pow.prepareData(pow.block.Nonce)
	hash := sha256.Sum256(data)
	hashInt.SetBytes(hash[:])

	isValid := hashInt.Cmp(pow.target) == -1 
	return isValid
}

transcation包:

package transaction

import (
	"crypto/sha256"
	"encoding/gob"
	"bytes"
	"fmt"
	"log"
)

var subsidy int = 50  //挖矿奖励
/*创建一个交易的数据结构,交易是由交易ID、交易输入、交易输出组成的,
一个交易有多个输入和多个输出,所以这里的交易输入和输出应该是切片类型的
*/
type Transaction struct {
	ID		[]byte
	Vin		[]TXInput
	Vout	[]TXOutput
}

/*
1、每一笔交易的输入都会引用之前交易的一笔或多笔交易输出
2、交易输出保存了输出的值和锁定该输出的信息
3、交易输入保存了引用之前交易输出的交易ID、具体到引用
该交易的第几个输出、能正确解锁引用输出的签名信息
*/
//交易输出
type TXOutput struct {
	Value			int	//输出的值(可以理解为金额)
	ScriptPubKey	string	// 锁定该输出的脚本(目前还没实现地址,所以还不能锁定该输出为具体哪个地址所有)
}
//交易输入
type TXInput struct {
	Txid 	[]byte //引用的之前交易的ID
	Vout	int 	//引用之前交易输出的具体是哪个输出(一个交易中输出一般有很多)
	ScriptSig	string  // 能解锁引用输出交易的签名脚本(目前还没实现地址,所以本章不能实现此功能)
}

/*
	区块链上存储的交易都是由这些输入输出交易所组成的,
一个输入交易必须引用之前的输出交易,一个输出交易会被之后的输入所引用。
    问题来了,在最开始的区块链上是先有输入还是先有输出喃?
答案是先有输出,因为是区块链的创世区块产生了第一个输出,
这个输出也就是我们常说的挖矿奖励-狗头金,每一个区块都会有一个这样的输出,
这是奖励给矿工的交易输出,这个输出是凭空产生的。
*/
//现在我们来创建一个这样的coinbase挖矿输出
//to 代表此输出奖励给谁,一般都是矿工地址,data是交易附带的信息
func NewCoinbaseTX(to,data string) *Transaction {
	if data == "" {
		data = fmt.Sprintf("奖励给 '%s'",to)
	}
	//此交易中的交易输入,没有交易输入信息
	txin := TXInput{[]byte{},-1,data}
	//交易输出,subsidy为奖励矿工的币的数量
	txout := TXOutput{subsidy,to}
	//组成交易
	tx := Transaction{nil,[]TXInput{txin},[]TXOutput{txout}}
	//设置该交易的ID
	tx.SetID()
	return &tx
}
//设置交易ID,交易ID是序列化tx后再哈希
func (tx *Transaction) SetID() {
	var hash [32]byte
	var encoder bytes.Buffer

	enc := gob.NewEncoder(&encoder)
	err := enc.Encode(tx)
	if err != nil {
		log.Panic(err)
	}
	hash = sha256.Sum256(encoder.Bytes())
	tx.ID =  hash[:]
}

/*
1、每一个区块至少存储一笔coinbase交易,所以我们在区块的字段中把Data字段换成交易。
2、把所有涉及之前Data字段都要换了,比如NewBlock()、GenesisBlock()、pow里的函数
*/

//定义在输入和输出上的锁定和解锁方法,目的是让用户只能花自己所用于地址上的币
//输入上锁定的秘钥,表示能引用的输出是unlockingData
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
	return in.ScriptSig == unlockingData
}
//输出上的解锁秘钥,表示能被引用的输入是unlockingData
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
	return out.ScriptPubKey == unlockingData 
}

//判断是否为coinbase交易
func (tx *Transaction) IsCoinbase() bool {
	return len(tx.Vin) == 1 && len(tx.Vin[0].Txid) == 0 && tx.Vin[0].Vout == -1
}

CLI包:

package CLI

import (
	"go_code/A_golang_blockchain/transaction"
	"fmt"
	"os"
	"flag"
	"go_code/A_golang_blockchain/blockchain"
	"go_code/A_golang_blockchain/pow"
	"strconv"
	"log"
)
//首先我们想要拥有这些命令 1.加入区块命令 2.打印区块链命令

//创建一个CLI结构体
type CLI struct {
	//BC *blockchain.Blockchain
}


//加入输入格式错误信息提示
func (cli *CLI) printUsage() {
	fmt.Println("Usage:")
	fmt.Println("  getbalance -address ADDRESS  得到该地址的余额")
	fmt.Println("  createblockchain -address ADDRESS 创建一条链并且该地址会得到狗头金")
	fmt.Println("  printchain - 打印链")
	fmt.Println("  send -from FROM -to TO -amount AMOUNT 地址from发送amount的币给地址to")
}

//判断命令行参数,如果没有输入参数则显示提示信息
func (cli *CLI) validateArgs() {
	if len(os.Args) < 2 {
		cli.printUsage()
		os.Exit(1)
	}
}

// //加入区块函数调用
// func (cli *CLI) addBlock(data string) {
// 	cli.BC.MineBlock(data)
// 	fmt.Println("成功加入区块...")
// }

//打印区块链函数调用
func (cli *CLI) printChain() {
	//实例化一条链
	bc := blockchain.NewBlockchain("")  //因为已经有了链,不会重新创建链,所以接收的address设置为空
	defer bc.Db().Close()

	//这里需要用到迭代区块链的思想
	//创建一个迭代器
	bci := bc.Iterator()

	for {

		block := bci.Next()	//从顶端区块向前面的区块迭代

		fmt.Printf("时间戳:%v\n",block.Timestamp)
		fmt.Printf("PrevHash:%x\n",block.PrevBlockHash)
		//fmt.Printf("Data:%s\n",block.Data)
		fmt.Printf("Hash:%x\n",block.Hash)
		//验证当前区块的pow
		pow := pow.NewProofOfWork(block)
		boolen := pow.Validate()
		fmt.Printf("POW is %s\n",strconv.FormatBool(boolen))
		fmt.Println()
		
		if len(block.PrevBlockHash) == 0 {
			break
		}
	}
}

//创建一条链
func (cli *CLI) createBlockchain(address string) {
	bc := blockchain.NewBlockchain(address)
	bc.Db().Close()
	fmt.Println("Done!")
}
//求账户余额
func (cli *CLI) getBalance(address string) {
	bc := blockchain.NewBlockchain(address)
	defer bc.Db().Close()

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

	//遍历UTXOs中的交易输出out,得到输出字段out.Value,求出余额
	for _,out := range UTXOs {
		balance += out.Value
	}

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

//send方法
func (cli *CLI) send(from,to string,amount int) {
	bc := blockchain.NewBlockchain(from)
	defer bc.Db().Close()

	tx := blockchain.NewUTXOTransaction(from,to,amount,bc)
	//挖出一个包含该交易的区块,此时区块只有这一个交易
	bc.MineBlock([]*transaction.Transaction{tx})
	fmt.Println("发送成功...")
}

//入口函数
func (cli *CLI) Run() {
	//判断命令行输入参数的个数,如果没有输入任何参数则打印提示输入参数信息
	cli.validateArgs()
	//实例化flag集合
	getBalanceCmd := flag.NewFlagSet("getbalance", flag.ExitOnError)
	createBlockchainCmd := flag.NewFlagSet("createblockchain", flag.ExitOnError)
	sendCmd := flag.NewFlagSet("send", flag.ExitOnError)
	printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
	//注册flag标志符
	getBalanceAddress := getBalanceCmd.String("address", "", "The address to get balance for")
	createBlockchainAddress := createBlockchainCmd.String("address", "", "The address to send genesis block reward to")
	sendFrom := sendCmd.String("from", "", "Source wallet address")
	sendTo := sendCmd.String("to", "", "Destination wallet address")
	sendAmount := sendCmd.Int("amount", 0, "Amount to send")
	
	switch os.Args[1] {		//os.Args为一个保存输入命令的切片
	case "getbalance":
		err := getBalanceCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	case "createblockchain":
		err := createBlockchainCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	case "printchain":
		err := printChainCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	case "send":
		err := sendCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	default:
		cli.printUsage()
		os.Exit(1)
	}

	//进入被解析出的命令,进一步操作
	if getBalanceCmd.Parsed() {
		if *getBalanceAddress == "" {
			getBalanceCmd.Usage()
			os.Exit(1)
		}
		cli.getBalance(*getBalanceAddress)
	}

	if createBlockchainCmd.Parsed() {
		if *createBlockchainAddress == "" {
			createBlockchainCmd.Usage()
			os.Exit(1)
		}
		cli.createBlockchain(*createBlockchainAddress)
	}

	if printChainCmd.Parsed() {
		cli.printChain()
	}

	if sendCmd.Parsed() {
		if *sendFrom == "" || *sendTo == "" || *sendAmount <= 0 {
			sendCmd.Usage()
			os.Exit(1)
		}
		cli.send(*sendFrom, *sendTo, *sendAmount)
	}
}

main包:

package main

import (
	"go_code/A_golang_blockchain/CLI"
)

	func main() {
		// bc := Blockchain.NewBlockchain()
		// defer bc.Db().Close()
	
		cli := CLI.CLI{}
		cli.Run()
	
	}

在敲代码的过程中我已经把大多数比较难理解的代码都进行了详细的注解,看明白应该不是什么问题;

下面是测试结果:

需要注意的是我们在编写过程中测试的时候会生成blockchain.db数据库,在最后的代码测试我们最好在main.go目录下面把blockchain.db删除,然后重新create区块链。

——如果有发现上面错误或者是有疑问,可以联系我。联系方式(微信 18382255942)谢谢!!!

猜你喜欢

转载自blog.csdn.net/zyj0813/article/details/82077749