title: Golang实现区块链(四)—交易 tags: go,blockchain
到目前为止我们已经实现了区块链的持久化和交互界面。但是比特币中最核心的交易功能,我们还没能实现它,本章就将对区块链交易功能进行实现。
在实现交易功能之前,我们首先了解下比特币的交易原理,比特币采用的是 UTXO 模型,并非账户模型,并不直接存在“余额”这个概念,余额需要通过遍历整个交易历史得来。
我们平常使用支付宝、微信钱包等都是账户模型,它们都有类似的两张表,account和transaction。account表用于存放用户信息和余额,而金额交易记录会存在transaction表。
UTXO 是 Unspent Transaction Output(未消费交易输出),UTXO是中本聪最早在比特币中采用的一个具体的技术方案。
在比特币的设计中,并没有账户的概念,如果你想要知道自己的比特币钱包中多少个比特币,UTXO的思路是:
查看有多少笔交易给你比特币并且你还没有花费掉,你没有花费掉的总额就是你带余额。
上面那段话理解起来是不是很绕?别急我们慢慢来说。
花费是什么概念,每一个Transaction Output都犹如现实中的一张纸币,他只有两种状态,属于你或者不属于你。 未花费就是该张纸币属于你,已花费就是该张纸币不属于你。
我们用现实中的例子抽象一下:
假如你父亲给你50元(未花费),你用这50元买了一瓶5块(花费)钱的饮料,你现在这个交易还没有花费掉的钱有45元。然后你妈妈又给(未花费)了你100元,你用了60元买(花费)了学习资料,那么这个交易还没有花费掉的钱有40元。按照UTXO来,你的余额就是45+40 =85。
在刚刚到例子中,我们存在着找零的现象。那么在比特币中是否也存在呢?没错比特币中也存在找零这个概念。但是这个找零概念跟我们理解的又有点不同。
其实并不只是找零,如果用兜里一把零碎utxo去转账,反而是找回一个整的。
其实这个就相当于我们现实生活中的找商家换钱,我们家里经常攒着一堆硬币,所以我们想找个商家给它们换成面值大的纸币,但又不好意思让商家直接换,所以会购买某个廉价的商品,然后顺便提取换币的要求。商家接过你的硬币,并减去你刚刚消费的,会把你剩下了的钱,给你换成面值大的纸币。 这就是UTXO一个抽象的找零过程。
上面的例子中,父母给钱和买东西,代表比特币代码中的input和output。 父母给你钱这笔钱对他们来说就是output(输出),对你而言就是input(输入)。 对于每一笔新的交易,它的输入会引用之前一笔交易的输出(这里有个例外,coinbase 交易,coinbase是创世区块生成的,所以它没有输出),引用就是花费的意思。所谓引用之前的一个输出,也就是将之前的一个输出包含在另一笔交易的输入当中,就是花费之前的交易输出。交易的输出,就是币实际存储的地方。下面的图示阐释了交易之间的互相关联:
注意: 1.有一些输出并没有被关联到某个输入上 2.一笔交易的输入可以引用之前多笔交易的输出 3.一个输入必须引用一个输出
一笔交易由一些输入(input)和输出(output)组合而来:
type Transaction struct{ // tx hash(交易的唯一标识) TxHash []byte // 输入 Vins []*TxInput // 输出 Vouts []*TxOutput }在比特币中交易输出主要包含两个部分: 1.一定的比特币,用来消费的钱 2.一个锁定脚本,要花这笔钱,必须解锁这个脚本
// 交易输出 type TxOutput struct { // 1. 有多少钱(金额) Value int64 // 2. 钱是谁的(用户名) ScriptPubkey string } }实际上,正是输出里面存储了“币”(注意,也就是上面的 Value 字段)。而这里的存储,指的是用一个数学难题对输出进行锁定,这个难题被存储在 ScriptPubKey 里面。在内部,比特币使用了一个叫做 Script 的脚本语言,用它来定义锁定和解锁输出的逻辑。
由于我们还没有实现地址,所以目前 ScriptPubkey 将仅仅存储一个用户自定义的任意钱包地址.
在比特币中交易输出主要包含三个部分: 1.存储的是之前交易的hash 2.上一笔交易的output索引 3.解锁脚本
// 交易输入 type TxInput struct { // 交易哈希(不是当前交易的哈希) TxHash []byte // 引用的上一笔交易的output索引 Vout int // 用户名 ScriptSig string }ScriptSig 是一个脚本,提供了可解锁输出结构里面 ScriptPubKey 字段的数据。如果 ScriptSig 提供的数据是正确的,那么输出就会被解锁,然后被解锁的值就可以被用于产生新的输出;如果数据不正确,输出就无法被引用在输入中,或者说,无法使用这个输出。这种机制,保证了用户无法花费属于其他人的币。
由于我们还没有实现地址,所以目前 ScriptSig 将仅仅存储一个用户自定义的任意钱包地址
前面我们说过“对于每一笔新的交易,它的输入会引用之前一笔交易的output(这里有个例外,coinbase 交易)” 。什么是coinbase 交易呢?“coinbase transaction”是一种特殊类型的交易,它不需要任何output,他是由创世区块生成的,所以没有比它更早的output了。
// 生成coinbase交易 func NewCoinbaseTransaction(address string) *Transaction { // 输入 txInput := &TxInput{[]byte{}, -1, "Genesis Data"} // 输出 txOutput := &TxOutput{10, address} txCoinbase := &Transaction{nil,[]*TxInput{txInput}, []*TxOutput{txOutput}} // hash txCoinbase.HashTransaction() return txCoinbase }一个coinbase交易只能有一个input。在我们的实现里,TxHash是空的,Vout是-1。另外,coinbase也不需要存储ScriptSig。相反,有任意的数据存储在这里。
在我们之前的区块设计中,用了Data来代表交易信息,现在我们已经实现了Transaction,并且现在只能通过交易来挖出新的区块,因此我们应该用Transaction来替换Data。
// 实现一个最基本的区块结构 type Block struct { TimeStamp int64 // 区块时间戳,区块产生的时间 Heigth int64 // 区块高度(索引、号码),代表当前区块的高度 PrevBlockHash []byte // 前一个区块(父区块)的哈希 Hash []byte // 当前区块的哈希 //Data []byte // 交易数据 Txs []*Transaction // 交易数据 Nonce int64 // 用于生成工作量证明的哈希 }随着Block的更改,我们其他都代码也需要进行更改。
// 创建新的区块 //data被替换成了txs func NewBlock(height int64, prevBlockHash []byte, txs []*Transaction) *Block { var block Block block = Block{Heigth:height,PrevBlockHash:prevBlockHash,Txs:txs,TimeStamp:time.Now().Unix()} //block.SetHash() // 生成区块当前哈希 pow := NewProofOfWork(&block) hash, nonce := pow.Run() // 解题(执行工作量证明算法) block.Hash = hash block.Nonce = nonce return &block }更改创世区块
// 生成创世区块 func CreateGenesisBlock(txs []*Transaction) *Block { return NewBlock(1,nil,txs) }添加区块也要更改
// 添加新的区块到区块链中 func (bc *BlockChain) AddBlock(txs []*Transaction /*替换参数*/) { // 更新数据 err := bc.DB.Update(func(tx *bolt.Tx) error { // 1 获取数据表 b := tx.Bucket([]byte(blockTableName)) if nil != b { // 2. 确保表存在 // 3. 获取最新区块的哈希 // newEstHash := b.Get([]byte("l")) blockBytes := b.Get(bc.Tip) latest_block := DeserializeBlock(blockBytes) // 4. 创建新区块 newBlock := NewBlock(latest_block.Heigth + 1, latest_block.Hash, txs) // 创建一个新的区块 // 5. 存入数据库 err := b.Put(newBlock.Hash, newBlock.Serialize()) if nil != err { log.Panicf("put the data of new block into db failed! %v\n", err) } // 6. 更新最新区块的哈希 err = b.Put([]byte("l"), newBlock.Hash) if nil != err { log.Panicf("put the hash of the newest block into db failed! %v\n", err) } bc.Tip = newBlock.Hash } return nil }) if nil != err { log.Panicf("update the db of block failed! %v\n",err) } }当然我也要更改CLI区块添加的功能
// 添加区块 func (cli *CLI) addBlock(txs []*Transaction) { if dbExists() == false { fmt.Println("数据库不存在...") os.Exit(1) } blockchain := BlockchainObject() // 获取区块链对象 defer blockchain.DB.Close() blockchain.AddBlock(txs) }我们前面说过,Coinbase Transaction是创世区块产生的,所以我们要在区块链初始化中添加NewCoinbaseTransaction函数,并且重新设置创世区块的生成。
// 初始化区块链 func CreateBlockChainWithGenesisBlock(address string) *BlockChain { if dbExists() { fmt.Println("创世区块已存在...") os.Exit(1) // 退出 } // 创建或者打开数据 db, err := bolt.Open(dbName, 0600,nil) if nil != err { log.Panicf("open the db failed! %v\n", err) } //defer db.Close() var blockHash []byte // 需要存储到数据库中的区块哈希 err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if nil == b { // 添加创世区块 b, err = tx.CreateBucket([]byte(blockTableName)) if nil != err { log.Panicf("create the bucket [%s] failed! %v\n", blockTableName, err) } } if nil != b { // 生成交易 txCoinbase := NewCoinbaseTransaction(address) // 生成创世区块( genesisBlock := CreateGenesisBlock([]*Transaction{txCoinbase}) err = b.Put(genesisBlock.Hash, genesisBlock.Serialize()) if nil != err { log.Panicf("put the data of genesisBlock to db failed! %v\n", err) } // 存储最新区块的哈希 err = b.Put([]byte("l"), genesisBlock.Hash) if nil != err { log.Panicf("put the hash of latest block to db failed! %v\n", err) } blockHash = genesisBlock.Hash } return nil }) if nil != err { log.Panicf("update the data of genesis block failed! %v\n", err) } return &BlockChain{db, blockHash} }回想下我们之前的代码,在进行工作量证明之前,我们在准备的数据中加入了pow.Block.Data。现在我们已经使用了Txs, 但是因为Txs是结构体,不是我们需要的[]byte,所以我们现在把区块中的所有交易结构转换成[]byte。
// 把区块中的所有交易结构转换成[]byte func (block *Block) HashTransactions() []byte { var txHashes [][]byte for _, tx := range block.Txs { txHashes = append(txHashes,tx.TxHash) } // sha256 txHash := sha256.Sum256(bytes.Join(txHashes, []byte{})) return txHash[:] }现在我们来替换掉pow.Block.Data
// 准备数据,将区块相差属性搭接越来,返回一个字节数组 func (pow *ProofOfWork) prepareData(nonce int) []byte { data := bytes.Join([][]byte{ pow.Block.PrevBlockHash, //pow.Block.Data; pow.Block.HashTransactions(), IntToHex(pow.Block.TimeStamp), IntToHex(pow.Block.Heigth), IntToHex(int64(nonce)), IntToHex(targetBit), },[]byte{}) return data }通过CLI获取余额
// 查询余额 func (cli *CLI) getBalance(from string) { // 获取指定地址的余额 outPuts := UnUTXOS(from) fmt.Printf("unUTXO : %v\n", outPuts) }要想发送交易,首先我们要获取到Blockchain对象
// 返回Blockchain 对象 func BlockchainObject() *BlockChain { // 读取数据库 db, err := bolt.Open(dbName, 0600, nil) if nil != err { log.Panicf("get the object of blockchain failed! %v\n", err) } var tip []byte // 最新区块的哈希值 err = db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if nil != b { tip = b.Get([]byte("l")) } return nil }) return &BlockChain{db, tip} }现在,我们要把币送给其它人。为了实现这个,需要创建一笔交易,把它设到区块中,然后挖出这个区块。到目前为止,我们的代码也只是实现了coinbase交易,现在需要一个普通的交易。
/ 挖矿(生成新的区块) // 通过接收交易,进行打包确认,最终生成新的区块 func (blockchain *BlockChain)MineNewBlock(from, to, amount []string) { fmt.Printf("\tFROM:[%s]\n", from) fmt.Printf("\tTO:[%s]\n", to) fmt.Printf("\tAMOUNT:[%s]\n", amount) // 接收交易 var txs []*Transaction // 要打包的交易列表 value, _ := strconv.Atoi(amount[0]) tx := NewSimpleTransaction(from[0], to[0], value) txs = append(txs, tx) // 打包交易 // 生成新的区块 var block *Block // 从数据库中获取最新区块 blockchain.DB.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if nil != b { hash := b.Get([]byte("l")) // 获取最新区块哈希值(当作新生区块的prevHash) blockBytes := b.Get(hash) // 得到最新区块(为了获取区块高度) block = DeserializeBlock(blockBytes) // 反序列化 } return nil }) // 生成新的区块 block = NewBlock(block.Heigth + 1, block.Hash, txs) // 持久化新区块 blockchain.DB.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if nil != b { err := b.Put(block.Hash,block.Serialize()) if nil != err { log.Panicf("update the new block to db failed! %v\n", err) } b.Put([]byte("l"), block.Hash) // 更新数据库中的最新哈希值 blockchain.Tip = block.Hash } return nil }) }接着我们在CLI交互中定义 发送方法
// 发送交易 func (cli *CLI) send(from, to, amount []string) { // 检测数据库 if dbExists() == false { fmt.Println("数据库不存在...") os.Exit(1) } blockchain := BlockchainObject() // 获取区块链对象 defer blockchain.DB.Close() blockchain.MineNewBlock(from, to, amount) }传送币到其它地址,意味着会创建新的交易,然后会通过挖出新的区块,把交易放到该区块中,再把该区块放到区块链的方式让交易得以在区块链中。但是区块链并不会立即做到这一步,相反,它把所有的交易放到存储池中,当矿机准备好挖区块时,它就把存储池中的所有交易拿出来并创建候选的区块。交易只有在包含了该交易的区块被挖出且附加到区块链中时才会被确认。
我们总算实现了交易功能。尽管关键的特性像比特币那样的加密货币还没有实现:我们还没有实现真正的地址、挖矿奖励。