🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 前言 前面为了转账,需要迭代整个区块链,查找未花费的输出,这要求发起交易者必须运行一个全节点,截止 2017 年 9 月 18 日,在比特币中已经有 485,860 个块,整个数据库所需磁盘空间超过 140 Gb。这是非常困难的。 整个问题的解决方案是有一个仅有未花费输出的索引,这就是 UTXO 集要做的事情:这是一个从所有区块链交易中构建(对区块进行迭代,但是只须在创建区块链时候做一次)而来的缓存,然后用它来计算余额和验证新的交易。截止 2017 年 9 月,UTXO 集大概有 2.7 Gb,远小于整个节点。 # 顺便实现下挖矿奖励 我们的区块链由交易发起者自行完成挖矿,所以奖励给到交易发起者即可。 挖矿奖励,实际上就是一笔 coinbase 交易。 ~~~ func (cli *CLI) send(from, to string, amount int) { ... bc := NewBlockchain() UTXOSet := UTXOSet{bc} defer bc.db.Close() tx := NewUTXOTransaction(from, to, amount, &UTXOSet) cbTx := NewCoinbaseTX(from, "")//创建一个coninbase交易,给from发送一个固定数量的奖励。 txs := []*Transaction{cbTx, tx}//将coninbase交易和实际交易打包到一起 newBlock := bc.MineBlock(txs)//挖矿 fmt.Println("交易成功!") } ~~~ >bitcoin的挖矿由专职矿工完成。交易发起者将交易丢进交易池中,矿工从交易池中取出一部分交易,打包挖矿,获得奖励。奖励币数量是不固定的。 # 实现UTXO UTXO集定义 ~~~ type UTXOSet struct { Blockchain *Blockchain } ~~~ UTXO元素是一个区块链Blockchain的引用 ## 取得UTXO并存储到新的bucket中 ~~~ func (u UTXOSet) Reindex() { db := u.Blockchain.Db//与区块链使用相同的数据库 bucketName := []byte(utxoBucket)//新的bucket err := db.Update(func(tx *bolt.Tx) error { err := tx.DeleteBucket(bucketName)//如果存在,则删除 _, err = tx.CreateBucket(bucketName)//创建新的bucket }) UTXO := u.Blockchain.FindUTXO()//获得所有未花费输出 err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket(bucketName) for txID, outs := range UTXO { key, err := hex.DecodeString(txID) err = b.Put(key, outs.Serialize())//将交易ID-输出存储到bucket } }) } ~~~ 这个方法仅仅在区块链新建完成后,唯一一次被调用:当一个新的区块链被创建以后,就会立刻进行重建索引。 ~~~ func (cli *CLI) createBlockchain(address string) { ... bc := CreateBlockchain(address) defer bc.db.Close() UTXOSet := UTXOSet{bc} UTXOSet.Reindex() ... } ~~~ ## 转账的新方式~~~ func (u UTXOSet) FindSpendableOutputs(pubkeyHash []byte, amount int) (int, map[string][]int) { unspentOutputs := make(map[string][]int) accumulated := 0 db := u.Blockchain.Db err := db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(utxoBucket))//从bucket中读取UTXO集 c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() {//循环UTXO集 txID := hex.EncodeToString(k) outs := DeserializeOutputs(v) for outIdx, out := range outs.Outputs { if out.IsLockedWithKey(pubkeyHash) && accumulated < amount { accumulated += out.Value unspentOutputs[txID] = append(unspentOutputs[txID], outIdx) } } } }) return accumulated, unspentOutputs } ~~~ ## 查询余额的新方式 ~~~ func (u UTXOSet) FindUTXO(pubKeyHash []byte) []TXOutput { var UTXOs []TXOutput db := u.Blockchain.Db err := db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(utxoBucket))//从bucket中读取UTXO集 c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() {//循环UTXO集 outs := DeserializeOutputs(v) for _, out := range outs.Outputs { if out.IsLockedWithKey(pubKeyHash) { UTXOs = append(UTXOs, out) } } } return nil }) return UTXOs } ~~~ ## 同步机制 当挖出一个新块时,应该更新 UTXO 集。 更新意味着移除已花费输出,并从新挖出来的交易中加入未花费输出。 ~~~ ~~~ func (u UTXOSet) Update(block *Block) {//block为挖出的新区块 db := u.Blockchain.db err := db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(utxoBucket)) for _, tx := range block.Transactions {//迭代新区块里面的所有交易 if tx.IsCoinbase() == false {//coinbase交易不更新到UTXO集中?那么挖矿奖励金怎么办? for _, vin := range tx.Vin { updatedOuts := TXOutputs{}//交易ID为vin.Txid的新的UTXO,将替换原来数据库中交易ID为vin.Txid的UTXO outsBytes := b.Get(vin.Txid)//从数据库读取的UTXO集中获得被包含到输入的输出交易ID,一个vin中只有一个输出索引 outs := DeserializeOutputs(outsBytes) for outIdx, out := range outs.Outputs {//迭代数据库该交易的所有索引的输出。输出集合可能有多个索引对应的输出 if outIdx != vin.Vout { //如果某条索引对于的输出没有被包含在输入中,加入到新的最终输出集合中。 //注意,包含到输入中输出最小单位是输出的某条索引对应的输出(索引从0开始) updatedOuts.Outputs = append(updatedOuts.Outputs, out)//加入到新的已经花费的输出组中 } } if len(updatedOuts.Outputs) == 0 { //一个交易ID为vin.Txid的交易完成后,交易的输出索引对应的输出将被移除,如果结果是,该交易不再包含任何输出, //那么这笔交易的输出应该被直接从UTXO数据库中移除(UTXO链没有必要留着空的节点)。 err := b.Delete(vin.Txid) } else { //如果交易ID为vin.Txid的交易在完成后,仍然有输出,则更新到数据库,替换该交易原来的UTXO集合 err := b.Put(vin.Txid, updatedOuts.Serialize()) } } } //当然,交易将产生新的输出,直接加入到UTXO中即可 newOutputs := TXOutputs{} for _, out := range tx.Vout {//迭代Vout,获得输出索引对应的输出 newOutputs.Outputs = append(newOutputs.Outputs, out) } err := b.Put(tx.ID, newOutputs.Serialize())//tx.ID为当前交易的ID } }) } ~~~ # Merkle树实现 上如上面所提到的,完整的比特币数据库(也就是区块链)需要超过 140 Gb 的磁盘空间。因为比特币的去中心化特性,网络中的每个节点必须是独立,自给自足的,也就是每个节点必须存储一个区块链的完整副本。随着越来越多的人使用比特币,这条规则变得越来越难以遵守:因为不太可能每个人都去运行一个全节点。并且,由于节点是网络中的完全参与者,它们负有相关责任:节点必须验证交易和区块。另外,要想与其他节点交互和下载新块,也有一定的网络流量需求。 在中本聪的[比特币原始论文](https://link.jianshu.com/?t=https://bitcoin.org/bitcoin.pdf)中,对这个问题也有一个解决方案:简易支付验证(Simplified Payment Verification, SPV)。SPV 是比特币轻节点,它不需要下载整个区块链,也**不需要验证区块和交易**。相反,它会在区块链查找交易(为了验证支付),并且需要连接到一个全节点来检索必要的数据。这个机制允许在全网只运行一个全节点的情况下有多个轻节点(轻钱包)。 为了实现 SPV,**需要有一个方式来检查是否一个区块包含了某笔交易,而无须下载整个区块。这就是 Merkle 树所要完成的事情。** 比特币用 Merkle 树来获取交易哈希,哈希被保存在区块头中,并会用于工作量证明系统。 Merkle 树的好处就是一个节点可以在不下载整个块的情况下,验证是否包含某笔交易。并且这些只需要一个当前交易哈希,一个 Merkle 树根哈希和一个 Merkle 路径(向全网请求获得路径),然后本地计算,即可检查是否包含了某笔交易。 # 运行检验 ## 转账 ![](https://img.kancloud.cn/20/91/20912871fa21fccf1fb65cc58c602945_1580x117.png) 转账结果: ![](https://img.kancloud.cn/d6/f9/d6f9da32dcf17f2c91bcb39031299508_1108x229.png) 之所以转账后from的币数量是17,是因为转账挖矿奖励10币,加上转账余额7币,一共17个币。 ## 打印区块链 ![](https://img.kancloud.cn/89/a0/89a06f821fd71033c1fc13550055993f_1962x1223.png) # 优化Findtranaction 在很多场合需要根据交易ID,查找交易,函数Findtrasaction功能即如此。 为此,我们将<tx.ID,block.Hash>保存到数据库,这样,我们通过数据库表utxoBlockBucket的key(tx.ID)可以查询到block的ID,然后通过blocksBucket的Get,可以快速获得交易。(原来是通过迭代blockchain来完成,效率很低)。这样,也为后面轻节点通过网络请求得到交易留下优化空间。 优化之后,节点仍然需要下载整个区块链,才能查询到交易。 ## 表 const utxoBlockBucket = "chainstate\_blockid2tx" ## 修改Reindex ![](https://img.kancloud.cn/e3/ed/e3ed0a7ea3cc1435d3eaeb637f7bcdf3_1642x978.png) 将UTXO和UTXOBlock都写入数据库 ## 修改同步函数 ![](https://img.kancloud.cn/74/2c/742c57b30cdcf7356ad9d0acd51a3a71_908x286.png) 更新UTXO的同时,同步UTXOBlock ## 修改FindtransactionForUTXO ![](https://img.kancloud.cn/37/3e/373ef208664da14d14b1017fc113af81_1337x1473.png) ## 修改其它代码