区块链的作用就是要实现交易,一种无法篡改永久保存的交易。比特币区块链还没有像以太坊一样已经实现了账户的概念,比特币区块链上的交易双方是地址,地址背后才是人,人和地址不是一一对应的关系,一个人可以拥有很多比特币的地址。
——注:底端有完整的代码,代码里面有详细的注解,可以直接看完整的代码,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),当然我们查询余额往往不能查询链上所有地址的余额,我们只能查询我们自己所拥有地址的余额,换句话说我们所查询的未花费输出的余额都是我们自己能解锁的那部分余额。目前我们还没有涉及到秘钥,所以我们只