Go语言实现区块链(四)

一、比特币交易介绍

1. 比特币交易与传统银行交易的区别?

传统银行的每个账户都有一个数据库保存用户的信息,例如:姓名、卡号、余额等信息。每产生一笔交易,银行系统都会更新用户账户的余额字段。

比特币的数据库只有交易,没有用户帐号信息。那么比特币系统如何维护用户的比特币呢?

image-20181207221818512

不像银行系统,比特币系统中每个交易都是以之前的交易为基础。用户的比特币零散分布在不同的交易中。如果想要知道某个钱包地址的余额,那么就需要遍历整个账本,把属于该地址的交易进行统计。

 

2. 找零机制

比如张三钱包中有100个比特币,转给了李四80个比特币。就相当于张三钱包中有一个100元的钞票,转给李四80元。那么剩下20元怎么办呢?

答案是,比特币系统会扣除李四的钱,然后再把剩余的前转给自己。这就是比特币的找零机制。

image-20180902175804340

 

3. 转账形式

比特币系统中没有付款人和收款人,只有输入(input)和输出(output)。每个输入都对应着之前别人给你转账时产生的某个输出。

(1)多对一转账(众筹)

image-20180902175832201

(2)一对多转账(发红包)

image-20180902175927530

(3)多对多转账

image-20181012111755026

 

4. 手续费

大多数交易包含交易费(矿工费),这是为了确保网络安全而给比特币矿工的一种补偿。费用本身也作为一个安全机制,使经济上不利于攻击者通过交易来淹没网络。

交易费是按照交易数据大小来计算,而不是比特币交易的价值。

交易的数据结构没有交易费的字段。相替代地,交易费是指输入和输出之间的差值。从所有输入中扣掉所有输出之后的多余的量会被矿工作为矿工费收集走。

计算公式:交易费 = 所有输入 - 所有输出

 

5. 比特币交易结构

比特币交易包含输入input和输出output两部分。

(1)交易输出

比如张三给李四转账,比特币系统会生成一个output,这个output包含两样东西:“转账金额”和“锁定脚本”。

转账金额:比特币个数

锁定脚本:李四的公钥哈希(可以理解为使用李四的公钥进行加密,只有李四的私钥才可以解出来)

(2)交易输入

与output对应的是input,每一个input都来自于一个output。例如李四给王五转账,系统会创建input,这个input包含了:

所引用交易的ID:在哪一笔交易中,即需要张三->李四这笔转账的交易ID

所引用交易的OUTPUT的索引:

解锁脚本:张三的签名和公钥。签名是用于认证这笔钱是我的,公钥是用于找到引用的output。

注意:挖矿奖励不需要引用任何output,相当于有一个特殊的input。

完整的校验脚本如下图所示:

image-20180908174756528

 

6. 未消费输出(UTXO)

这里就会涉及到output的消费问题,我们把尚未使用的output有个专用的名字,叫做未消费输出(unspent transaction output,UTXO)。UTXO是比特币交易中最小的支付单元,不可分割,每一个UTXO必须一次性消耗完,然后生成新的UTXO,存放在比特币网络的UTXO池中。

"一个用户的比特币余额",这个概念是一个通过比特币钱包应用创建的派生之物。比特币钱包通过扫描区块链并聚合所有属于该用户的UTXO来计算该用户的余额。

 

二、继续完善功能

1. 添加交易

(1)定义交易结构体

type TXInput struct {
	TXID        []byte // 引用输出的交易ID
	OutputIndex int64  // 引用输出的索引
	//ScriptSig   string // 解锁脚本(实际上这里应该是签名和公钥)
	Signature []byte // 签名
	PubKey    []byte // 公钥
}

type TXOutput struct {
	Value float64 // 金额
	//ScriptPubKey string  // 锁定脚本(公钥哈希)
	PubKeyHash []byte // 公钥哈希
}

type Transaction struct {
	TXID    []byte     // 交易ID
	Inputs  []TXInput  // 交易输入
	Outputs []TXOutput // 交易输出
}

(2)设置交易ID

// 设置交易ID
func (tx *Transaction) SetHash() {
	// 对tx进行加密处理,生成字节流
	var buffer bytes.Buffer
	encoder := gob.NewEncoder(&buffer)
	encoder.Encode(&tx)
	// 使用sha256对字节流再次假面,生成一个hash值
	data := buffer.Bytes()
	hash := sha256.Sum256(data)
	// 把hash作为交易的ID
	tx.TXID = hash[:]
}

(3)创建CoinBase挖矿交易

const reward = 50 // 挖矿奖励

// 创建挖矿交易
// 参数一:矿工地址
// 参数二:矿工附加信息
func NewCoinBaseTx(address string, data string) *Transaction {
	input := TXInput{nil, -1, data}
	output := TXOutput{reward, address}
	tx := Transaction{nil, []TXInput{input}, []TXOutput{output}}
	tx.SetHash()
	return &tx
}

(4)改写Block结构体,使用交易数组代替Data。

// 定义区块
type Block struct {
	// 版本号
	Version uint64
	// 前区块哈希值
	PrevHash []byte
	// 梅克尔根
	MerKleRoot []byte
	// 时间戳
	TimeStamp uint64
	// 难度值
	Difficulty uint64
	//随机数
	Nonce uint64
	// 当前区块哈希值
	Hash []byte
	// 区块数据
	//Data []byte
	Data []*Transaction // 区块的交易数据
}

(5)改写NewBlock函数

// 创建方法
func NewBlock(data []*Transaction, prevHash []byte) *Block {
	block := Block{
		Version:    00,
		PrevHash:    prevHash,
		MerKleRoot: []byte{},
		TimeStamp:  uint64(time.Now().Unix()),
		Difficulty: 1,
		Nonce:      1,
		//Data: []byte(data),
		Data: data,
	}

	//提供一个设置哈希的方法
	//block.SetHash()
	pow := NewProofOfWork(block)
	hash, nonce := pow.Run()
	block.Hash = hash
	block.Nonce = nonce
	return &block
}

(6)改写NewBlockChain函数,添加address参数,并且调用NewCoinBase创建并添加挖矿交易。

// 创建方法
//func NewBlockChain() *BlockChain {
func NewBlockChain(address string) *BlockChain {
	/*// 创建创世块
	genericBlock := NewBlock([]byte(genesisInfo), []byte{})
	// 创建BlockChain
	bc := BlockChain{[]*Block{genericBlock}}
	return &bc*/

	var db *bolt.DB
	var lastHash []byte

	// 1. 打开数据库
	db, err := bolt.Open(dbName, 0600, nil)
	if err != nil {
		panic("bolt.Open err!")
	}
	db.Update(func(tx *bolt.Tx) error {
		// 2. 打开抽屉Bucket
		bucket := tx.Bucket([]byte(bucketName))
		// 3. 如果Bucket是否为nil,则创建一个Bucket
		if bucket == nil {
			bucket, err = tx.CreateBucket([]byte(bucketName))
			if err != nil {
				panic("tx.CreateBucket err!")
			}
			// 创建创世块
			//genericBlock := NewBlock([]byte(genesisInfo), []byte{})
			//创建coinbase交易  <---这里修改
			coinbaseTx := NewCoinBaseTx(address, genesisInfo)
			//创建创世块 <---这里修改
			genericBlock := NewBlock([]*Transaction{coinbaseTx}, []byte{})
			// 把创世块保存在bucket中
			bucket.Put(genericBlock.Hash, genericBlock.Serialize())
			// 把创世块的hash保存在last中
			bucket.Put([]byte(last), genericBlock.Hash)
			// 记录lastHash
			lastHash = genericBlock.Hash
		} else {
			// 4. 如果Bucket不为nil,记录lastHash
			lastHash = bucket.Get([]byte(last))
		}
		return nil
	})
	return &BlockChain{db, lastHash}
}

(7)改写AddBlock函数,把参数data修改为txs。

// 添加区块
//func (bc *BlockChain) AddBlock(data string) {
func (bc *BlockChain) AddBlock(txs []*Transaction) {
	/*// 获取最后区块
	lastBlock := bc.Blocks[len(bc.Blocks)-1]
	// 创建一个新区块
	block := NewBlock([]byte(data), lastBlock.Hash)
	// 添加新区块
	bc.Blocks = append(bc.Blocks, block)*/

	// 获取最后区块hash
	lastHash := bc.Tail
	// 创建区块
	//block := NewBlock([]byte(data), lastHash)
	block := NewBlock(txs, lastHash)
	// 更新操作
	bc.Db.Update(func(tx *bolt.Tx) error {
		bucket := tx.Bucket([]byte(bucketName))
		if bucket == nil {
			panic("bucket should not be nil!")
		}
		// 向bolt数据库添加新区块
		bucket.Put(block.Hash, block.Serialize())
		// 更新数据库的last
		bucket.Put([]byte(last), block.Hash)
		// 更新bc.Tail
		bc.Tail = block.Hash
		return nil
	})
}

(8)修改PrepareData函数,把block.Data注释掉。

// 根据nonce生成区块哈希,该方法与SetHash方法类似
func (pow *ProofOfWork) PrepareData(nonce uint64) []byte {
	block := pow.Block
	block.MerKleRoot = block.HashTransactions()
	tmp := [][]byte{
		block.PrevHash,
		//block.Data,
		block.MerKleRoot,
		uint64ToByte(block.Version),
		uint64ToByte(block.TimeStamp),
		uint64ToByte(block.Difficulty),
		uint64ToByte(nonce),
	}
	blockInfo := bytes.Join(tmp, []byte{})
	// 2.使用sha256加密
	hash := sha256.Sum256(blockInfo)
	return hash[:]
}
  • 在比特币中,其实是对区块头进行哈希运算,而不是对区块整体进行哈希运算。

  • 比特币系统根据交易信息生成Merkel Root哈希值,所以交易可以影响到区块的哈希值。

(9)添加HashTransaction函数

这个函数是为了生成Merkel Tree Root哈希值,正常的生成过程是使用所有交易的哈希值生成一个平衡二叉树,此处,为了简化代码,我们目前直接将区块中交易的哈希值进行拼接后进行哈希操作即可。

// 对区块中所有交易ID进行哈希运算,作为梅克尔根
func (block *Block)HashTransactions() []byte {
	var tmp [][]byte
	for _, tx := range block.Data {
		//交易的ID就是交易的哈希值,还记得吗?我们在Transaction里面提供了方法。
		tmp = append(tmp, tx.TXID)
	}
	data := bytes.Join(tmp, []byte{})
	hash := sha256.Sum256(data)
	return hash[:]
}

(10)修改命令行

把addBlock函数注释掉。

/*// 添加区块
func (cli *Cli) addBlock(data string) {
	cli.bc.AddBlock(data)
}*/

把常量Usage中的“block addBlock --data DATA”内容。

const Usage  = `
   block printChain "打印区块链" 
   block getBalance --address ADDRESS "查询账户余额"
   block send FROM TO AMOUNT MINER DATA "由FROM给TO转AMOUNT个比特币,并指定MINER为矿工"
   block newWallet "创建一个钱包"
   block listAddress "列出所有钱包地址"
`

修改run函数,把case "addblock"中的代码注释掉。

/*case "addBlock":
	if len(os.Args) == 4 && os.Args[2] == "--data" {
		data := os.Args[3]
		if data == "" {
			fmt.Println("data should not be empty!")
			return
		}
	cli.addBlock(data)
}*/

修改main函数,在调用NewBlockChain函数的时候传入矿工地址即可。

func main() {
	bc := NewBlockChain("张三")
	cli := Cli{bc}
	cli.Run()
}

 

2. 查询余额

比特币通过转账在系统中流通,付款人的钱包会使用付款人的解锁脚本解开能够支配的UTXO,完成花费。同时使用收款人的地址付款金额进行锁定,使之完成接收,从而实现金额的转移。

(1)解锁脚本

解锁脚本是检验input是否可以使用由某个地址锁定的utxo。所以对于解锁脚本来说,是外部提供锁定信息,我去检查一下能否解开它。

func (input *TXInput) CanUnlockUTXOWith(unlockData string /*收款人的地址*/) bool {
    //ScriptSig是签名,v4版本中使用付款人的地址填充
    return input.ScriptSig == unlockData
}

(2)锁定脚本

锁定脚本是用于指定比特币的新主人,在创建output的时候创建。对于这个output来说,它一直在等待一个签名的到来,检查这个签名能否解开自己锁定的比特币。

func (output *TXOutput) CanBeUnlockedWith(unlockData string/*付款人的地址(签名)*/) bool {
    //ScriptPubKey是锁定信息,v4版本中使用收款人的地址填充
    return output.ScriptPubKey == unlockData
}

(3)找出UTXO所在交易的集合

所有未花费的比特币都在UTXO中,UTXO又包含在交易中。所以如果要获取可以支配的UTXO,必须找到这些UTXO所在的交易。如果每个人有N个UTXO,所以我们需要找到所有的交易,也就是说,应该找到包含UTXO的交易的集合。

// 查询所有UTXO交易
// 参数:账户地址
func (bc *BlockChain) FindUTXOTransaction(address string) []Transaction {
	var txs []Transaction
	spentUTXOs := make(map[string][]int64) // 已消费的UTXO, Key代表交易地址,Value为引用output的索引
	// 遍历所有区块
	itr := bc.NewIterator()
	for {
		block := itr.Prev()
		// 遍历交易
		for _, tx := range block.Data {
			// 遍历output
		OUTPUT_TAG:
			for i, output := range tx.Outputs {
				if spentUTXOs[string(tx.TXID)] != nil {
					// 获取消费过的utxo的索引
					indexs := spentUTXOs[string(tx.TXID)]
					// 循环比较当前output的索引是否在indexs中存在,如果存在就代表该output已经被消费
					for _, index := range indexs {
						if index == int64(i) {
							continue OUTPUT_TAG
						}
					}
				}
				// 如果output的ScriptPubKey等于address,代表该output是属于adddres指定的账户
				if output.ScriptPubKey == address {
					txs = append(txs, *tx)
				}
			}

			// 遍历input
			if !tx.isCoinBase() {
				for _, input := range tx.Inputs {
					// 如果input的ScriptSig等于address,就代表该input是属于指定address的账户
					if input.ScriptSig == address {
						spentUTXOs[string(input.TXID)] = append(spentUTXOs[string(input.TXID)], int64(input.OutputIndex))
					}
				}
			}
		}
		if (len(block.PrevHash) == 0) {
			return txs
		}
	}
}

(4)定义isCoinBase函数

// 判断当前交易是否是挖矿交易
func (tx *Transaction) isCoinBase() bool {
	return len(tx.Inputs) == 1 && tx.Inputs[0].TXID == nil && tx.Inputs[0].OutputIndex == -1
}

(5)获取指定地址的UTXO的集合

// 查找所有的utxo
func (bc *BlockChain) FindUTXOs(address string) []TXOutput {
	txs := bc.FindUTXOTransaction(address)
	var outputs []TXOutput
	// 遍历所有utxos交易
	for _, tx := range txs {
		// 遍历utxo交易的所有output
		for _, output := range tx.Outputs {
			// 如果output的ScriptPubKey等于address,就代表该output是我们要找到output
			if output.ScriptPubKey == address {
				outputs = append(outputs, output)
			}
		}
	}
	return outputs
}

(6)添加GetBalance函数,用于获取余额。

// 查询余额
func (cli *Cli) GetBalance(address string) {
	// 查询所有未消费的output
	utxos := cli.bc.FindUTXOs(address)
	var total float64
	for _, output := range utxos {
		total += output.Value
	}
	fmt.Printf("%s的余额: %f\n", address, total)
}

(7)修改Run函数,加入getBalance的case判断。

func (cli *Cli) Run() {
	args := os.Args
	if len(args) < 2 {
		fmt.Println(Usage)
		return
	}
	// 获取命令名称
	command := args[1]
	switch command {
		case "printChain":
			cli.printChain()
		case "getBalance":
			if len(os.Args) == 4 && os.Args[2] == "--address" {
				address := os.Args[3]
				if len(address) == 0 {
					fmt.Println("地址不能为空!")
					return
				}
				cli.GetBalance(address)
			} else {
				fmt.Println(Usage)
			}
		default:
			fmt.Println(Usage)
	}
}

 

3. 交易转账

(1)创建交易

// 创建交易
func NewTransaction(from, to string, amount float64, bc *BlockChain) *Transaction {
	// 1.找到最优的utxos
	utxos, total := bc.FindNeedUTXOs(from, amount)
	// 2.检查余额是否足够
	if total < amount {
		fmt.Println("余额不足!")
		return nil
	}
	// 3.如果余额足够,那么创建新的区块
	var inputs []TXInput
	var outputs []TXOutput

	for txId, outputIndexs := range utxos {
		for _, outputIndex := range outputIndexs {
			input := TXInput{[]byte(txId), outputIndex, from}
			inputs = append(inputs, input)
		}
	}

	output := TXOutput{amount, to}
	outputs = append(outputs, output)
	// 找零
	if total > amount {
		output = TXOutput{total - amount, from}
		outputs = append(outputs, output)
	}
	// 4.创建Transaction
	tx := Transaction{nil, inputs, outputs}
	tx.SetHash()

	return &tx
}

(2)定义FindNeedUTXOs函数

// 查找最合理的utxo
func (bc *BlockChain) FindNeedUTXOs(address string, amount float64) (map[string][]int64, float64) {
	txs := bc.FindUTXOTransaction(address)
	needUTXOs := make(map[string][]int64) // 保存最合理的utxo
	var total float64                     // 最合理utxo的总金额
OUTPUT_TAG:
	for _, tx := range txs {
		for i, output := range tx.Outputs {
			if output.ScriptPubKey == address {
				if total < amount {
					total += output.Value
					needUTXOs[string(tx.TXID)] = append(needUTXOs[string(tx.TXID)], int64(i))
				} else {
					break OUTPUT_TAG
				}
			}
		}
	}
	return needUTXOs, total
}

(3)添加Send命令

// 转账方法
func (cli *Cli) Send(from, to string, amount float64, miner string, data string) {
	// 创建交易
	tx := NewTransaction(from, to, amount, cli.bc)
	// 创建挖矿交易
	coinbase := NewCoinBaseTx(miner, data)
	// 添加区块
	cli.bc.AddBlock([]*Transaction{coinbase, tx})
	fmt.Println("转账成功...")
	// 打印余额
	cli.GetBalance(from)
}

(4)修改Run方法,加入case send条件判断。

func (cli *Cli) Run() {
	args := os.Args
	if len(args) < 2 {
		fmt.Println(Usage)
		return
	}
	// 获取命令名称
	command := args[1]
	switch command {
        ...
        ...
		case  "send":
			args := os.Args
			if len(args) < 7 {
				fmt.Println("参数不正确!")
				fmt.Println(Usage)
				return
			}
			//block send FROM TO AMOUNT MINER DATA "由FROM给TO转AMOUNT个比特币,并指定MINER为矿工"
			from := args[2]
			to := args[3]
			num, _ := strconv.ParseFloat(args[4], 16)
			miner := args[5]
			data := args[6]
			cli.Send(from, to , num, miner, data)
		default:
			fmt.Println(Usage)
	}
}

 

 

猜你喜欢

转载自blog.csdn.net/zhongliwen1981/article/details/89637251