在这个系列文章的一开始,我们就提到了,区块链是一个分布式数据库。不过在之前的文章中,我们选择性地跳过了“分布式”这个部分,而是将注意力都放到了“数据库”部分。到目前为止,我们几乎已经实现了一个区块链数据库的所有元素(基本原型,工作量证明,持久化,命令行接口,交易,地址,数字签名等)。今天,我们将会分析之前跳过的一些机制。而在下一篇文章中,我们将会开始讨论区块链的分布式特性。

1. 课程目标

  1. 知道什么是奖励

  2. 知道什么是UTXO集

  3. 学会UTXO集优化的原理

  4. 项目中修改代码实现UTXO集查询余额

  5. 项目中修改代码实现UTXO集进行转账交易

2. 项目代码及效果展示

2.1 项目代码结构

2.2 项目运行结果

创建钱包地址,创建创世区块和使用UTXOSet优化后的转账,查询余额效果图如下:

3. 创建项目

3.1 创建工程

打开IntelliJ IDEA的工作空间,将上一个项目代码目录part7_Signature,复制为part8_Transaction2

然后打开IntelliJ IDEA开发工具。

打开工程:part8_Transaction2,并删除target目录。然后进行以下修改:

step1:先将项目重新命名为:part8_Transaction2。step2:修改pom.xml配置文件。 改为:<artifactId>part8_Transaction2artifactId>标签 改为:<name>part8_Transaction2 Maven Webappname>

3.2 代码实现

3.2.1 修改java文件:RocksDBUtils.java

打开cldy.hanru.blockchain.store包,修改RocksDBUtils.java文件:

修改步骤:

修改步骤:step1:添加String CHAINSTATE_BUCKET_KEY = "chainstate";step2:添加private Map<String, byte[]> chainstateBucket;step3:添加initChainStateBucket()方法step4:添加cleanChainStateBucket()方法step5:添加putUTXOs()step6:添加getUTXOs()step7:添加deleteUTXOs()step8:修改RocksDBUtils()构造函数

修改完后代码如下:

package cldy.hanru.blockchain.store;

import cldy.hanru.blockchain.block.Block;import cldy.hanru.blockchain.transaction.TXOutput;import cldy.hanru.blockchain.util.SerializeUtils;import lombok.Getter;import lombok.extern.slf4j.Slf4j;import org.rocksdb.RocksDB;import org.rocksdb.RocksDBException;

import java.util.HashMap;import java.util.Map;

/** * 数据库存储的工具类 * @author hanru */@Slf4jpublic class RocksDBUtils {/** * 区块链数据文件 */private static final String DB_FILE = "blockchain.db";/** * 区块桶前缀 */private static final String BLOCKS_BUCKET_KEY = "blocks";/** * 链状态桶Key */private static final String CHAINSTATE_BUCKET_KEY = "chainstate";

/** * 最新一个区块的hash */private static final String LAST_BLOCK_KEY = "l";

private volatile static RocksDBUtils instance;/** * 获取RocksDBUtils的单例 * @return */public static RocksDBUtils getInstance() {if (instance == null) { synchronized (RocksDBUtils.class) {if (instance == null) { instance = new RocksDBUtils(); } } }return instance; }

private RocksDBUtils() { openDB(); initBlockBucket(); initChainStateBucket(); }private RocksDB db;

/** * block buckets */private Mapbyte[]> blocksBucket;/** * 打开数据库 */private void openDB() {try { db = RocksDB.open(DB_FILE); } catch (RocksDBException e) {throw new RuntimeException("打开数据库失败。。 ! ", e); } }/** * 初始化 blocks 数据桶 */private void initBlockBucket() {try {//byte[] blockBucketKey = SerializeUtils.serialize(BLOCKS_BUCKET_KEY);byte[] blockBucketBytes = db.get(blockBucketKey);if (blockBucketBytes != null) { blocksBucket = (Map) SerializeUtils.deserialize(blockBucketBytes); } else { blocksBucket = new HashMap<>(); db.put(blockBucketKey, SerializeUtils.serialize(blocksBucket)); } } catch (RocksDBException e) {throw new RuntimeException("初始化block的bucket失败。。! ", e); } }/** * 保存区块 * * @param block */public void putBlock(Block block) {try { blocksBucket.put(block.getHash(), SerializeUtils.serialize(block)); db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket)); } catch (RocksDBException e) {throw new RuntimeException("存储区块失败。。 ", e); } }/** * 查询区块 * * @param blockHash * @return */public Block getBlock(String blockHash) {return (Block) SerializeUtils.deserialize(blocksBucket.get(blockHash)); }/** * 保存最新一个区块的Hash值 * * @param tipBlockHash */public void putLastBlockHash(String tipBlockHash) {try { blocksBucket.put(LAST_BLOCK_KEY, SerializeUtils.serialize(tipBlockHash)); db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket)); } catch (RocksDBException e) {throw new RuntimeException("数据库存储最新区块hash失败。。 ", e); } }/** * 查询最新一个区块的Hash值 * * @return */public String getLastBlockHash() {byte[] lastBlockHashBytes = blocksBucket.get(LAST_BLOCK_KEY);if (lastBlockHashBytes != null) {return (String) SerializeUtils.deserialize(lastBlockHashBytes); }return ""; }/** * 关闭数据库 */public void closeDB() {try { db.close(); } catch (Exception e) {throw new RuntimeException("关闭数据库失败。。 ", e); } }/** * chainstate buckets */ @Getterprivate Mapbyte[]> chainstateBucket;/** * 初始化 blocks 数据桶 */private void initChainStateBucket() {try {byte[] chainstateBucketKey = SerializeUtils.serialize(CHAINSTATE_BUCKET_KEY);byte[] chainstateBucketBytes = db.get(chainstateBucketKey);if (chainstateBucketBytes != null) { chainstateBucket = (Map) SerializeUtils.deserialize(chainstateBucketBytes); } else { chainstateBucket = new HashMap<>(); db.put(chainstateBucketKey, SerializeUtils.serialize(chainstateBucket)); } } catch (RocksDBException e) { log.error("Fail to init chainstate bucket ! ", e);throw new RuntimeException("Fail to init chainstate bucket ! ", e); } }/** * 清空chainstate bucket */public void cleanChainStateBucket() {try { chainstateBucket.clear(); } catch (Exception e) { log.error("Fail to clear chainstate bucket ! ", e);throw new RuntimeException("Fail to clear chainstate bucket ! ", e); } }/** * 保存UTXO数据 * * @param key 交易ID * @param utxos UTXOs */public void putUTXOs(String key, TXOutput[] utxos) {try { chainstateBucket.put(key, SerializeUtils.serialize(utxos)); db.put(SerializeUtils.serialize(CHAINSTATE_BUCKET_KEY), SerializeUtils.serialize(chainstateBucket)); } catch (Exception e) { log.error("Fail to put UTXOs into chainstate bucket ! key=" + key, e);throw new RuntimeException("Fail to put UTXOs into chainstate bucket ! key=" + key, e); } }/** * 查询UTXO数据 * * @param key 交易ID */public TXOutput[] getUTXOs(String key) {byte[] utxosByte = chainstateBucket.get(key);if (utxosByte != null) {return (TXOutput[]) SerializeUtils.deserialize(utxosByte); }return null; }/** * 删除 UTXO 数据 * * @param key 交易ID */public void deleteUTXOs(String key) {try { chainstateBucket.remove(key); db.put(SerializeUtils.serialize(CHAINSTATE_BUCKET_KEY), SerializeUtils.serialize(chainstateBucket)); } catch (Exception e) { log.error("Fail to delete UTXOs by key ! key=" + key, e);throw new RuntimeException("Fail to delete UTXOs by key ! key=" + key, e); } }}

3.2.2 创建UTXOSet.java文件

打开cldy.hanru.blockchain.transaction包,创建UTXOSet.java文件。

添加代码如下:

package cldy.hanru.blockchain.transaction;

import cldy.hanru.blockchain.block.Block;import cldy.hanru.blockchain.block.Blockchain;import cldy.hanru.blockchain.store.RocksDBUtils;import cldy.hanru.blockchain.util.SerializeUtils;import lombok.AllArgsConstructor;import lombok.NoArgsConstructor;import lombok.Synchronized;import lombok.extern.slf4j.Slf4j;import org.apache.commons.codec.binary.Hex;import org.apache.commons.lang3.ArrayUtils;

import java.util.HashMap;import java.util.Map;

/** * 未被花费的交易输出池 * @author hanru */@NoArgsConstructor@AllArgsConstructor@Slf4jpublic class UTXOSet {

private Blockchain blockchain;

/** * 重建 UTXO 池索引 */@Synchronizedpublic void reIndex() { log.info("Start to reIndex UTXO set !"); RocksDBUtils.getInstance().cleanChainStateBucket(); Map allUTXOs = blockchain.findAllUTXOs();for (Map.Entry entry : allUTXOs.entrySet()) { RocksDBUtils.getInstance().putUTXOs(entry.getKey(), entry.getValue()); } log.info("ReIndex UTXO set finished ! "); }/** * 查找钱包地址对应的所有UTXO * * @param pubKeyHash 钱包公钥Hash * @return */public TXOutput[] findUTXOs(byte[] pubKeyHash) { TXOutput[] utxos = {}; Mapbyte[]> chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket();if (chainstateBucket.isEmpty()) {return utxos; }for (byte[] value : chainstateBucket.values()) { TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(value);for (TXOutput txOutput : txOutputs) {if (txOutput.isLockedWithKey(pubKeyHash)) { utxos = ArrayUtils.add(utxos, txOutput); } } }return utxos; }/** * 寻找能够花费的交易 * * @param pubKeyHash 钱包公钥Hash * @param amount 花费金额 */public SpendableOutputResult findSpendableOutputs(byte[] pubKeyHash, int amount) { Mapint[]> unspentOuts = new HashMap<>();int accumulated = 0; Mapbyte[]> chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket();for (Map.Entrybyte[]> entry : chainstateBucket.entrySet()) { String txId = entry.getKey(); TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(entry.getValue());for (int outId = 0; outId < txOutputs.length; outId++) { TXOutput txOutput = txOutputs[outId];if (txOutput.isLockedWithKey(pubKeyHash) && accumulated < amount) { accumulated += txOutput.getValue();int[] outIds = unspentOuts.get(txId);if (outIds == null) { outIds = new int[]{outId}; } else { outIds = ArrayUtils.add(outIds, outId); } unspentOuts.put(txId, outIds);if (accumulated >= amount) {break; } } } }return new SpendableOutputResult(accumulated, unspentOuts); }/** * 更新UTXO池 * 当一个新的区块产生时,需要去做两件事情: * 1)从UTXO池中移除花费掉了的交易输出; * 2)保存新的未花费交易输出; * * @param tipBlock 最新的区块 */@Synchronizedpublic void update(Block tipBlock) {if (tipBlock == null) { log.error("Fail to update UTXO set ! tipBlock is null !");throw new RuntimeException("Fail to update UTXO set ! "); }for (Transaction transaction : tipBlock.getTransactions()) {// 根据交易输入排查出剩余未被使用的交易输出if (!transaction.isCoinbase()) {for (TXInput txInput : transaction.getInputs()) {// 余下未被使用的交易输出 TXOutput[] remainderUTXOs = {}; String txId = Hex.encodeHexString(txInput.getTxId()); TXOutput[] txOutputs = RocksDBUtils.getInstance().getUTXOs(txId);if (txOutputs == null) {continue; }for (int outIndex = 0; outIndex < txOutputs.length; outIndex++) {if (outIndex != txInput.getTxOutputIndex()) { remainderUTXOs = ArrayUtils.add(remainderUTXOs, txOutputs[outIndex]); } }// 没有剩余则删除,否则更新if (remainderUTXOs.length == 0) { RocksDBUtils.getInstance().deleteUTXOs(txId); } else { RocksDBUtils.getInstance().putUTXOs(txId, remainderUTXOs); } } }// 新的交易输出保存到DB中 TXOutput[] txOutputs = transaction.getOutputs(); String txId = Hex.encodeHexString(transaction.getTxId()); RocksDBUtils.getInstance().putUTXOs(txId, txOutputs); } }}

3.2.3 修改Blockchain.java

打开cldy.hanru.blockchain.block包,修改Blockchain.java文件:

修改步骤:

修改步骤:step1:修改getAllSpentTXOs()方法step2:添加findAllUTXOs()方法step3:删除findUnspentTransactions()step4:删除findUTXO()step5:删除findSpendableOutputs()step6:修改mineBlock(),添加返回值step7:修改verifyTransactions()方法,添加判断是否是coinbase交易

修改完后代码如下:

package cldy.hanru.blockchain.block;

import cldy.hanru.blockchain.store.RocksDBUtils;import cldy.hanru.blockchain.transaction.SpendableOutputResult;import cldy.hanru.blockchain.transaction.TXInput;import cldy.hanru.blockchain.transaction.TXOutput;import cldy.hanru.blockchain.transaction.Transaction;import cldy.hanru.blockchain.util.ByteUtils;import lombok.AllArgsConstructor;import lombok.Data;import org.apache.commons.codec.binary.Hex;import org.apache.commons.lang3.ArrayUtils;import org.apache.commons.lang3.StringUtils;import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;

import java.util.Arrays;import java.util.HashMap;import java.util.List;import java.util.Map;

/** * 区块链 * * @author hanru */@Data@AllArgsConstructorpublic class Blockchain {

/** * 最后一个区块的hash */private String lastBlockHash;

/** * 创建区块链,createBlockchain * * @param address * @return */public static Blockchain createBlockchain(String address) {

 String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();if (StringUtils.isBlank(lastBlockHash)) {//对应的bucket不存在,说明是第一次获取区块链实例// 创建 coinBase 交易 Transaction coinbaseTX = Transaction.newCoinbaseTX(address, ""); Block genesisBlock = Block.newGenesisBlock(coinbaseTX); lastBlockHash = genesisBlock.getHash(); RocksDBUtils.getInstance().putBlock(genesisBlock); RocksDBUtils.getInstance().putLastBlockHash(lastBlockHash);

 }return new Blockchain(lastBlockHash); }

/** * 根据block,添加区块 * * @param block */public void addBlock(Block block) {

 RocksDBUtils.getInstance().putLastBlockHash(block.getHash()); RocksDBUtils.getInstance().putBlock(block);this.lastBlockHash = block.getHash();

 }

/** * 区块链迭代器:内部类 */public class BlockchainIterator {

/** * 当前区块的hash */private String currentBlockHash;

/** * 构造函数 * * @param currentBlockHash */public BlockchainIterator(String currentBlockHash) {this.currentBlockHash = currentBlockHash; }

/** * 判断是否有下一个区块 * * @return */public boolean hashNext() {if (ByteUtils.ZERO_HASH.equals(currentBlockHash)) {return false; } Block lastBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash);if (lastBlock == null) {return false; }// 如果是创世区块if (ByteUtils.ZERO_HASH.equals(lastBlock.getPrevBlockHash())) {return true; }return RocksDBUtils.getInstance().getBlock(lastBlock.getPrevBlockHash()) != null; }

/** * 迭代获取区块 * * @return */public Block next() { Block currentBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash);if (currentBlock != null) {this.currentBlockHash = currentBlock.getPrevBlockHash();return currentBlock; }return null; } }

/** * 添加方法,用于获取迭代器实例 * * @return */public BlockchainIterator getBlockchainIterator() {return new BlockchainIterator(lastBlockHash); }

/** * 打包交易,进行挖矿 * * @param transactions */public Block mineBlock(List transactions) throws Exception {// 挖矿前,先验证交易记录for (Transaction tx : transactions) {if (!this.verifyTransactions(tx)) {throw new Exception("ERROR: Fail to mine block ! Invalid transaction ! "); } } String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); Block lastBlock = RocksDBUtils.getInstance().getBlock(lastBlockHash);if (lastBlockHash == null) {throw new Exception("ERROR: Fail to get last block hash ! "); } Block block = Block.newBlock(lastBlockHash, transactions,lastBlock.getHeight()+1);this.addBlock(block);return block;

 }

/** * 依据交易ID查询交易信息 * * @param txId 交易ID * @return */private Transaction findTransaction(byte[] txId) throws Exception {

for (BlockchainIterator iterator = this.getBlockchainIterator(); iterator.hashNext(); ) { Block block = iterator.next();for (Transaction tx : block.getTransactions()) {if (Arrays.equals(tx.getTxId(), txId)) {return tx; } } }throw new Exception("ERROR: Can not found tx by txId ! "); }

/** * 从 DB 从恢复区块链数据 * * @return * @throws Exception */public static Blockchain initBlockchainFromDB() throws Exception { String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();if (lastBlockHash == null) {throw new Exception("ERROR: Fail to init blockchain from db. "); }return new Blockchain(lastBlockHash); }

/** * 进行交易签名 * * @param tx 交易数据 * @param privateKey 私钥 */public void signTransaction(Transaction tx, BCECPrivateKey privateKey) throws Exception {// 先来找到这笔新的交易中,交易输入所引用的前面的多笔交易的数据 Map prevTxMap = new HashMap<>();for (TXInput txInput : tx.getInputs()) { Transaction prevTx = this.findTransaction(txInput.getTxId()); prevTxMap.put(Hex.encodeHexString(txInput.getTxId()), prevTx); } tx.sign(privateKey, prevTxMap); }/** * 交易签名验证 * * @param tx */private boolean verifyTransactions(Transaction tx) throws Exception {if (tx.isCoinbase()) {return true; } Map prevTx = new HashMap<>();for (TXInput txInput : tx.getInputs()) { Transaction transaction = this.findTransaction(txInput.getTxId()); prevTx.put(Hex.encodeHexString(txInput.getTxId()), transaction); }try {return tx.verify(prevTx); } catch (Exception e) {throw new Exception("Fail to verify transaction ! transaction invalid ! "); } }/** * 从交易输入中查询区块链中所有已被花费了的交易输出 * * @return 交易ID以及对应的交易输出下标地址 */private Mapint[]> getAllSpentTXOs() {// 定义TxId ——> spentOutIndex[],存储交易ID与已被花费的交易输出数组索引值 Mapint[]> spentTXOs = new HashMap<>();for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { Block block = blockchainIterator.next();for (Transaction transaction : block.getTransactions()) {// 如果是 coinbase 交易,直接跳过,因为它不存在引用前一个区块的交易输出if (transaction.isCoinbase()) {continue; }for (TXInput txInput : transaction.getInputs()) { String inTxId = Hex.encodeHexString(txInput.getTxId());int[] spentOutIndexArray = spentTXOs.get(inTxId);if (spentOutIndexArray == null) { spentTXOs.put(inTxId, new int[]{txInput.getTxOutputIndex()}); } else { spentOutIndexArray = ArrayUtils.add(spentOutIndexArray, txInput.getTxOutputIndex()); } spentTXOs.put(inTxId, spentOutIndexArray); }// } } }return spentTXOs; }/** * 查找所有的 unspent transaction outputs * * @return */public Map findAllUTXOs() { Mapint[]> allSpentTXOs = this.getAllSpentTXOs(); Map allUTXOs = new HashMap<>();// 再次遍历所有区块中的交易输出for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { Block block = blockchainIterator.next();for (Transaction transaction : block.getTransactions()) { String txId = Hex.encodeHexString(transaction.getTxId());int[] spentOutIndexArray = allSpentTXOs.get(txId); TXOutput[] txOutputs = transaction.getOutputs();for (int outIndex = 0; outIndex < txOutputs.length; outIndex++) {if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) {continue; } TXOutput[] UTXOArray = allUTXOs.get(txId);if (UTXOArray == null) { UTXOArray = new TXOutput[]{txOutputs[outIndex]}; } else { UTXOArray = ArrayUtils.add(UTXOArray, txOutputs[outIndex]); } allUTXOs.put(txId, UTXOArray); } } }return allUTXOs; }}

3.2.4 修改Transaction.java文件

打开cldy.hanru.blockchain.block包,修改Transaction.java文件。

修改步骤:

修改步骤:step1:添加时间戳字段step2:修改newUTXOTransaction方法,从utxoSet中查找转账要使用的utxo。step3:修改:newCoinbaseTX()

修改完后代码如下:

package cldy.hanru.blockchain.transaction;

import cldy.hanru.blockchain.block.Blockchain;import cldy.hanru.blockchain.util.AddressUtils;import cldy.hanru.blockchain.util.SerializeUtils;import cldy.hanru.blockchain.wallet.Wallet;import cldy.hanru.blockchain.wallet.WalletUtils;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.apache.commons.codec.binary.Hex;import org.apache.commons.codec.digest.DigestUtils;import org.apache.commons.lang3.ArrayUtils;import org.apache.commons.lang3.StringUtils;import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;import org.bouncycastle.jce.ECNamedCurveTable;import org.bouncycastle.jce.provider.BouncyCastleProvider;import org.bouncycastle.jce.spec.ECParameterSpec;import org.bouncycastle.jce.spec.ECPublicKeySpec;import org.bouncycastle.math.ec.ECPoint;

import java.math.BigInteger;import java.security.KeyFactory;import java.security.PublicKey;import java.security.Security;import java.security.Signature;import java.util.Arrays;import java.util.Iterator;import java.util.Map;

/** * @author hanru */@Data@AllArgsConstructor@NoArgsConstructorpublic class Transaction {private static final int SUBSIDY = 10;

/** * 交易的Hash */private byte[] txId;/** * 交易输入 */private TXInput[] inputs;/** * 交易输出 */private TXOutput[] outputs;

/** * 创建日期 */private long createTime;

/** * 设置交易ID */private void setTxId() {this.setTxId(DigestUtils.sha256(SerializeUtils.serialize(this))); }

/** * 要签名的数据 * @return */public byte[] getData() {// 使用序列化的方式对Transaction对象进行深度复制byte[] serializeBytes = SerializeUtils.serialize(this); Transaction copyTx = (Transaction) SerializeUtils.deserialize(serializeBytes); copyTx.setTxId(new byte[]{});return DigestUtils.sha256(SerializeUtils.serialize(copyTx)); }

/** * 创建CoinBase交易 * * @param to 收账的钱包地址 * @param data 解锁脚本数据 * @return */public static Transaction newCoinbaseTX(String to, String data) {if (StringUtils.isBlank(data)) { data = String.format("Reward to '%s'", to); }// 创建交易输入

 TXInput txInput = new TXInput(new byte[]{}, -1, null, data.getBytes());// 创建交易输出 TXOutput txOutput = TXOutput.newTXOutput(SUBSIDY, to);// 创建交易 Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput}, System.currentTimeMillis());// 设置交易ID tx.setTxId();

return tx; }

/** * 从 from 向 to 支付一定的 amount 的金额 * * @param from 支付钱包地址 * @param to 收款钱包地址 * @param amount 交易金额 * @param blockchain 区块链 * @return */public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception {

// 获取钱包 Wallet senderWallet = WalletUtils.getInstance().getWallet(from);byte[] pubKey = senderWallet.getPublicKey();byte[] pubKeyHash = AddressUtils.ripeMD160Hash(pubKey);

 SpendableOutputResult result = new UTXOSet(blockchain).findSpendableOutputs(pubKeyHash, amount);int accumulated = result.getAccumulated(); Mapint[]> unspentOuts = result.getUnspentOuts();if (accumulated < amount) {throw new Exception("ERROR: Not enough funds"); } Iteratorint[]>> iterator = unspentOuts.entrySet().iterator(); TXInput[] txInputs = {};while (iterator.hasNext()) { Map.Entryint[]> entry = iterator.next(); String txIdStr = entry.getKey();int[] outIdxs = entry.getValue();byte[] txId = Hex.decodeHex(txIdStr);for (int outIndex : outIdxs) { txInputs = ArrayUtils.add(txInputs, new TXInput(txId, outIndex, null, pubKey)); } } TXOutput[] txOutput = {}; txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput(amount, to));if (accumulated > amount) { txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput((accumulated - amount), from)); } Transaction newTx = new Transaction(null, txInputs, txOutput, System.currentTimeMillis()); newTx.setTxId();// 进行交易签名 blockchain.signTransaction(newTx, senderWallet.getPrivateKey());return newTx; }/** * 是否为 Coinbase 交易 * * @return */public boolean isCoinbase() {return this.getInputs().length == 1 && this.getInputs()[0].getTxId().length == 0 && this.getInputs()[0].getTxOutputIndex() == -1; }/** * 创建用于签名的交易数据副本,交易输入的 signature 和 pubKey 需要设置为null * * @return */public Transaction trimmedCopy() { TXInput[] tmpTXInputs = new TXInput[this.getInputs().length];for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i]; tmpTXInputs[i] = new TXInput(txInput.getTxId(), txInput.getTxOutputIndex(), null, null); } TXOutput[] tmpTXOutputs = new TXOutput[this.getOutputs().length];for (int i = 0; i < this.getOutputs().length; i++) { TXOutput txOutput = this.getOutputs()[i]; tmpTXOutputs[i] = new TXOutput(txOutput.getValue(), txOutput.getPubKeyHash()); }return new Transaction(this.getTxId(), tmpTXInputs, tmpTXOutputs, this.getCreateTime()); }/** * 签名 * * @param privateKey 私钥 * @param prevTxMap 前面多笔交易集合 */public void sign(BCECPrivateKey privateKey, Map prevTxMap) throws Exception {// coinbase 交易信息不需要签名,因为它不存在交易输入信息if (this.isCoinbase()) {return; }// 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据for (TXInput txInput : this.getInputs()) {if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) {throw new Exception("ERROR: Previous transaction is not correct"); } }// 创建用于签名的交易信息的副本 Transaction txCopy = this.trimmedCopy(); Security.addProvider(new BouncyCastleProvider()); Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); ecdsaSign.initSign(privateKey);for (int i = 0; i < txCopy.getInputs().length; i++) { TXInput txInputCopy = txCopy.getInputs()[i];// 获取交易输入TxID对应的交易数据 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId()));// 获取交易输入所对应的上一笔交易中的交易输出 TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()]; txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); txInputCopy.setSignature(null);// 得到要签名的数据byte[] signData = txCopy.getData(); txInputCopy.setPubKey(null);// 对整个交易信息仅进行签名 ecdsaSign.update(signData);byte[] signature = ecdsaSign.sign();// 将整个交易数据的签名赋值给交易输入,因为交易输入需要包含整个交易信息的签名// 注意是将得到的签名赋值给原交易信息中的交易输入this.getInputs()[i].setSignature(signature); } }/** * 验证交易信息 * * @param prevTxMap 前面多笔交易集合 * @return */public boolean verify(Map prevTxMap) throws Exception {// coinbase 交易信息不需要签名,也就无需验证if (this.isCoinbase()) {return true; }// 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据for (TXInput txInput : this.getInputs()) {if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) {throw new Exception("ERROR: Previous transaction is not correct"); } }// 创建用于签名验证的交易信息的副本 Transaction txCopy = this.trimmedCopy(); Security.addProvider(new BouncyCastleProvider()); ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1"); KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i];// 获取交易输入TxID对应的交易数据 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId()));// 获取交易输入所对应的上一笔交易中的交易输出 TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()]; TXInput txInputCopy = txCopy.getInputs()[i]; txInputCopy.setSignature(null); txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());// 得到要签名的数据byte[] signData = txCopy.getData(); txInputCopy.setPubKey(null);// 使用椭圆曲线 x,y 点去生成公钥Key BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33)); BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65)); ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y); ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters); PublicKey publicKey = keyFactory.generatePublic(keySpec); ecdsaVerify.initVerify(publicKey); ecdsaVerify.update(signData);if (!ecdsaVerify.verify(txInput.getSignature())) {return false; } }return true; }}

3.2.5 修改CLI.java文件

打开cldy.hanru.blockchain.cli包,修改CLI.java文件。

修改步骤:

修改步骤:step1:修改getBalance()方法step2:修改send()方法

修改完后代码如下:

package cldy.hanru.blockchain.cli;

import cldy.hanru.blockchain.block.Block;import cldy.hanru.blockchain.block.Blockchain;import cldy.hanru.blockchain.pow.ProofOfWork;import cldy.hanru.blockchain.store.RocksDBUtils;import cldy.hanru.blockchain.transaction.TXInput;import cldy.hanru.blockchain.transaction.TXOutput;import cldy.hanru.blockchain.transaction.Transaction;import cldy.hanru.blockchain.transaction.UTXOSet;import cldy.hanru.blockchain.util.Base58Check;import cldy.hanru.blockchain.wallet.Wallet;import cldy.hanru.blockchain.wallet.WalletUtils;import lombok.extern.slf4j.Slf4j;import org.apache.commons.cli.*;import org.apache.commons.codec.binary.Hex;import org.apache.commons.lang3.StringUtils;import org.apache.commons.lang3.math.NumberUtils;

import java.text.SimpleDateFormat;import java.util.*;

/** * @author hanru */@Slf4jpublic class CLI {private String[] args;private Options options = new Options();

public CLI(String[] args) {this.args = args;

 Option helpCmd = Option.builder("h").desc("show help").build(); options.addOption(helpCmd);

 Option address = Option.builder("address").hasArg(true).desc("Source wallet address").build(); Option sendFrom = Option.builder("from").hasArg(true).desc("Source wallet address").build(); Option sendTo = Option.builder("to").hasArg(true).desc("Destination wallet address").build(); Option sendAmount = Option.builder("amount").hasArg(true).desc("Amount to send").build();

 options.addOption(address); options.addOption(sendFrom); options.addOption(sendTo); options.addOption(sendAmount); }

/** * 打印帮助信息 */private void help() {

 System.out.println("Usage:"); System.out.println(" createwallet - Generates a new key-pair and saves it into the wallet file"); System.out.println(" printaddresses - print all wallet address"); System.out.println(" getbalance -address ADDRESS - Get balance of ADDRESS"); System.out.println(" createblockchain -address ADDRESS - Create a blockchain and send genesis block reward to ADDRESS"); System.out.println(" printchain - Print all the blocks of the blockchain"); System.out.println(" send -from FROM -to TO -amount AMOUNT - Send AMOUNT of coins from FROM address to TO"); System.exit(0); }

/** * 验证入参 * * @param args */private void validateArgs(String[] args) {if (args == null || args.length < 1) { help(); } }

/** * 命令行解析入口 */public void run() {this.validateArgs(args); CommandLineParser parser = new DefaultParser(); CommandLine cmd = null;try { cmd = parser.parse(options, args); } catch (ParseException e) { e.printStackTrace(); }

switch (args[0]) {case "createblockchain": String createblockchainAddress = cmd.getOptionValue("address");if (StringUtils.isBlank(createblockchainAddress)) { help(); }this.createBlockchain(createblockchainAddress);break;case "getbalance": String getBalanceAddress = cmd.getOptionValue("address");if (StringUtils.isBlank(getBalanceAddress)) { help(); }try {this.getBalance(getBalanceAddress); } catch (Exception e) { e.printStackTrace(); }finally { RocksDBUtils.getInstance().closeDB(); }break;case "send": String sendFrom = cmd.getOptionValue("from"); String sendTo = cmd.getOptionValue("to"); String sendAmount = cmd.getOptionValue("amount");if (StringUtils.isBlank(sendFrom) || StringUtils.isBlank(sendTo) || !NumberUtils.isDigits(sendAmount)) { help(); }try {this.send(sendFrom, sendTo, Integer.valueOf(sendAmount)); } catch (Exception e) { e.printStackTrace(); }finally { RocksDBUtils.getInstance().closeDB(); }break;case "printchain":this.printChain();break;case "h":this.help();break;

case "createwallet":try {this.createWallet(); } catch (Exception e) { e.printStackTrace(); }break;case "printaddresses":try {this.printAddresses(); } catch (Exception e) { e.printStackTrace(); }break;default:this.help(); }

 }

/** * 创建区块链 * * @param address */private void createBlockchain(String address) {

 Blockchain blockchain = Blockchain.createBlockchain(address); UTXOSet utxoSet = new UTXOSet(blockchain); utxoSet.reIndex(); log.info("Done ! "); }

/** * 打印出区块链中的所有区块 */private void printChain() { Blockchain blockchain = null;try { blockchain = Blockchain.initBlockchainFromDB(); } catch (Exception e) { e.printStackTrace(); }

 Blockchain.BlockchainIterator iterator = blockchain.getBlockchainIterator();long index = 0;while (iterator.hashNext()) { Block block = iterator.next(); System.out.println("第" + block.getHeight() + "个区块信息:");

if (block != null) { boolean validate = ProofOfWork.newProofOfWork(block).validate(); System.out.println("validate = " + validate); System.out.println("\tprevBlockHash: " + block.getPrevBlockHash());

 System.out.println("\tTransaction: ");for (Transaction tx : block.getTransactions()) { System.out.printf("\t\t交易ID:%s\n", Hex.encodeHexString(tx.getTxId())); System.out.println("\t\t输入:");for (TXInput in : tx.getInputs()) { System.out.printf("\t\t\tTxID:%s\n", Hex.encodeHexString(in.getTxId())); System.out.printf("\t\t\tOutputIndex:%d\n", in.getTxOutputIndex());

 System.out.printf("\t\t\tPubKey:%s\n", Hex.encodeHexString(in.getPubKey())); } System.out.println("\t\t输出:");for (TXOutput out : tx.getOutputs()) { System.out.printf("\t\t\tvalue:%d\n", out.getValue());

 System.out.printf("\t\t\tPubKeyHash:%s\n", Hex.encodeHexString(out.getPubKeyHash())); }

 }

 System.out.println("\tHash: " + block.getHash()); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String date = sdf.format(new Date(block.getTimeStamp() * 1000L)); System.out.println("\ttimeStamp:" + date); System.out.println(); } } }

/** * 查询钱包余额 * * @param address 钱包地址 */private void getBalance(String address) throws Exception {

// 检查钱包地址是否合法try { Base58Check.base58ToBytes(address); } catch (Exception e) {throw new Exception("ERROR: invalid wallet address"); } Blockchain blockchain = Blockchain.createBlockchain(address);// 得到公钥Hash值byte[] versionedPayload = Base58Check.base58ToBytes(address);byte[] pubKeyHash = Arrays.copyOfRange(versionedPayload, 1, versionedPayload.length);

 UTXOSet utxoSet = new UTXOSet(blockchain); TXOutput[] txOutputs = utxoSet.findUTXOs(pubKeyHash);int balance = 0;if (txOutputs != null && txOutputs.length > 0) {for (TXOutput txOutput : txOutputs) { balance += txOutput.getValue(); } } System.out.printf("Balance of '%s': %d\n", address, balance); }

/** * 转账 * * @param from * @param to * @param amount */private void send(String from, String to, int amount) throws Exception {// 检查钱包地址是否合法try { Base58Check.base58ToBytes(from); } catch (Exception e) {throw new Exception("ERROR: sender address invalid ! address=" + from); }// 检查钱包地址是否合法try { Base58Check.base58ToBytes(to); } catch (Exception e) {throw new Exception("ERROR: receiver address invalid ! address=" + to); }if (amount < 1) {throw new Exception("ERROR: amount invalid ! "); }

 Blockchain blockchain = Blockchain.createBlockchain(from);// 新交易 Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain);// 奖励 Transaction rewardTx = Transaction.newCoinbaseTX(from, ""); List transactions = new ArrayList<>(); transactions.add(transaction); transactions.add(rewardTx); Block newBlock = blockchain.mineBlock(transactions);new UTXOSet(blockchain).update(newBlock); log.info("Success!"); }/** * 创建钱包 * * @throws Exception */private void createWallet() throws Exception { Wallet wallet = WalletUtils.getInstance().createWallet(); System.out.println("wallet address : " + wallet.getAddress()); }/** * 打印钱包地址 * * @throws Exception */private void printAddresses() throws Exception { Set addresses = WalletUtils.getInstance().getAddresses();if (addresses == null || addresses.isEmpty()) { System.out.println("There isn't address");return; }for (String address : addresses) { System.out.println("Wallet address: " + address); } }}

3.2.6 修改blockchain.sh脚本文件

最后修改blockchain.sh脚本文件,修改后内容如下:

#!/bin/bashset -e

# Check if the jar has been built.if [ ! -e target/part8_Transaction2-jar-with-dependencies.jar ]; thenecho "Compiling blockchain project to a JAR" mvn package -DskipTestsfi

java -jar target/part8_Transaction2-jar-with-dependencies.jar "$@"

4. 优化内容讲解

4.1 奖励Reward

在上一篇文章中,我们略过的一个小细节是挖矿奖励。现在,我们已经可以来完善这个细节了。

挖矿奖励,实际上就是一笔coinbase 交易。当一个挖矿节点开始挖出一个新块时,它会将交易从队列中取出,并在前面附加一笔coinbase交易。coinbase 交易只有一个输出,里面包含了矿工的公钥哈希。

实现奖励,非常简单,更新CLI类即可,修改 send() 方法:

/** * 转账 * * @param from * @param to * @param amount */private void send(String from, String to, int amount) throws Exception {// 检查钱包地址是否合法try { Base58Check.base58ToBytes(from); } catch (Exception e) {throw new Exception("ERROR: sender address invalid ! address=" + from); }// 检查钱包地址是否合法try { Base58Check.base58ToBytes(to); } catch (Exception e) {throw new Exception("ERROR: receiver address invalid ! address=" + to); }if (amount < 1) {throw new Exception("ERROR: amount invalid ! "); }

 Blockchain blockchain = Blockchain.createBlockchain(from);// 新交易 Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain);// 奖励 Transaction rewardTx = Transaction.newCoinbaseTX(from, ""); List transactions = new ArrayList<>(); transactions.add(transaction); transactions.add(rewardTx); Block newBlock = blockchain.mineBlock(transactions);new UTXOSet(blockchain).update(newBlock); log.info("Success!"); }

现在我们的逻辑调整为 ,每次转账都会给与奖励,而奖励通过coinbase交易实现,为了让这些coinbase交易的交易ID不同,我们需要在Transaction类添加时间字段:

public class Transaction {private static final int SUBSIDY = 10;

...

/** * 创建日期 */private long createTime;}

接下来就需要修改创建coinbase交易和创建转账交易的方法,添加创建日期:

/** * 创建CoinBase交易 * * @param to 收账的钱包地址 * @param data 解锁脚本数据 * @return */public static Transaction newCoinbaseTX(String to, String data) {if (StringUtils.isBlank(data)) { data = String.format("Reward to '%s'", to); }// 创建交易输入 TXInput txInput = new TXInput(new byte[]{}, -1, null, data.getBytes());// 创建交易输出 TXOutput txOutput = TXOutput.newTXOutput(SUBSIDY, to);// 创建交易 Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput}, System.currentTimeMillis());// 设置交易ID tx.setTxId();

return tx; }

以及:

/** * 从 from 向 to 支付一定的 amount 的金额 * * @param from 支付钱包地址 * @param to 收款钱包地址 * @param amount 交易金额 * @param blockchain 区块链 * @return */public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception {

// 获取钱包 Wallet senderWallet = WalletUtils.getInstance().getWallet(from);byte[] pubKey = senderWallet.getPublicKey();byte[] pubKeyHash = AddressUtils.ripeMD160Hash(pubKey);

 SpendableOutputResult result = blockchain.findSpendableOutputs(pubKeyHash, amount);int accumulated = result.getAccumulated(); Mapint[]> unspentOuts = result.getUnspentOuts();if (accumulated < amount) {throw new Exception("ERROR: Not enough funds"); } Iteratorint[]>> iterator = unspentOuts.entrySet().iterator(); TXInput[] txInputs = {};while (iterator.hasNext()) { Map.Entryint[]> entry = iterator.next(); String txIdStr = entry.getKey();int[] outIdxs = entry.getValue();byte[] txId = Hex.decodeHex(txIdStr);for (int outIndex : outIdxs) { txInputs = ArrayUtils.add(txInputs, new TXInput(txId, outIndex, null, pubKey)); } } TXOutput[] txOutput = {}; txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput(amount, to));if (accumulated > amount) { txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput((accumulated - amount), from)); } Transaction newTx = new Transaction(null, txInputs, txOutput, System.currentTimeMillis()); newTx.setTxId();// 进行交易签名 blockchain.signTransaction(newTx, senderWallet.getPrivateKey());return newTx; }

在我们的实现中,创建交易的第一个人同时挖出了新块,所以会得到一笔奖励。

在项目添加了奖励之后,我们进行测试,首先在终端创建3个地址:

hanru:part8_Transaction2 ruby$ ./blockchain.sh hhanru:part8_Transaction2 ruby$ ./blockchain.sh createwallethanru:part8_Transaction2 ruby$ ./blockchain.sh createwallethanru:part8_Transaction2 ruby$ ./blockchain.sh printaddresses

运行结果如下:

接下来我们创建创世区块,并进行一次转账:

hanru:part8_Transaction2 ruby$ ./blockchain.sh createblockchain -address 1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXthanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXthanru:part8_Transaction2 ruby$ ./blockchain.sh send -from 1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt -to 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2 -amount 4hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXthanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2

运行结果如下:

说明:

  1. 比如创世区块中1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt地址有10个Token,

  2. 转账给19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2地址4个Token,

  3. 那么1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt的余额是16(转账找零6个,加上得到奖励10个),

  4. 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2的余额是4。

4.2 UTXO 集

在持久化的章节中,我们研究了 Bitcoin Core 是如何在一个数据库中存储块的,并且了解到区块被存储在 blocks 数据库,交易输出被存储在 chainstate 数据库。会回顾一下 chainstate 的结构:

  1. c + 32 字节的交易哈希 -> 该笔交易的未花费交易输出记录

  2. B + 32 字节的块哈希 -> 未花费交易输出的块哈希

在之前那篇文章中,虽然我们已经实现了交易,但是并没有使用 chainstate 来存储交易的输出。所以,接下来我们继续完成这部分。

chainstate 不存储交易。它所存储的是 UTXO 集,也就是未花费交易输出的集合。除此以外,它还存储了“数据库表示的未花费交易输出的块哈希”,不过我们会暂时略过块哈希这一点,因为我们还没有用到块高度(但是我们会在接下来的文章中继续改进)。

那么,我们为什么需要 UTXO 集呢?

来思考一下我们早先实现的在Blockchain类中的findUnspentTransactions() 方法:

/** * 查找钱包地址对应的所有未花费的交易 * * @param pubKeyHash 钱包公钥Hash * @return */

private Transaction[] findUnspentTransactions(byte[] pubKeyHash) throws Exception { Mapint[]> allSpentTXOs = this.getAllSpentTXOs(pubKeyHash); Transaction[] unspentTxs = {};// 再次遍历所有区块中的交易输出for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { Block block = blockchainIterator.next();for (Transaction transaction : block.getTransactions()) { String txId = Hex.encodeHexString(transaction.getTxId());int[] spentOutIndexArray = allSpentTXOs.get(txId);for (int outIndex = 0; outIndex < transaction.getOutputs().length; outIndex++) {if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) {continue; }// 保存不存在 allSpentTXOs 中的交易if (transaction.getOutputs()[outIndex].isLockedWithKey(pubKeyHash)) { unspentTxs = ArrayUtils.add(unspentTxs, transaction); } } } }return unspentTxs;}

这个函数找到有未花费输出的交易。由于交易被保存在区块中,所以它会对区块链里面的每一个区块进行迭代,检查里面的每一笔交易。截止 2017 年 9 月 18 日,在比特币中已经有 485,860 个块,整个数据库所需磁盘空间超过 140 Gb。这意味着一个人如果想要验证交易,必须要运行一个全节点。此外,验证交易将会需要在许多块上进行迭代。

整个问题的解决方案是有一个仅有未花费输出的索引,这就是 UTXO 集要做的事情:这是一个从所有区块链交易中构建(对区块进行迭代,但是只须做一次)而来的缓存,然后用它来计算余额和验证新的交易。截止 2017 年 9 月,UTXO 集大概才有 2.7 Gb。

好了,让我们来想一下,为了实现 UTXOs 池我们需要做哪些事情。当前,有下列方法被用于查找交易信息:

  1. Blockchain.getAllSpentTXOs —— 查询所有已被花费的交易输出。它需要遍历区块链中所有区块中交易信息。

  2. Blockchain.findUnspentTransactions —— 查询包含未被花费的交易输出的交易信息。它也需要遍历区块链中所有区块中交易信息。

  3. Blockchain.findSpendableOutputs —— 该方法用于新的交易创建之时。它需要找到足够多的交易输出,以满足所需支付的金额。需要调用 Blockchain.findUnspentTransactions 方法。

  4. Blockchain.findUTXO —— 查询钱包地址所对应的所有未花费交易输出,然后用于计算钱包余额。需要调用

    Blockchain.findUnspentTransactions 方法。

  5. Blockchain.findTransaction —— 通过交易ID查询交易信息。它需要遍历所有的区块直到找到交易信息为止。

如你所见,上面这些方法都需要去遍历数据库中的所有区块。由于UTXOs池只存储未被花费的交易输出,而不会存储所有的交易信息,因此我们不会对有 Blockchain.findTransaction 进行优化。

那么,我们需要下列这些方法:

  1. Blockchain.findUTXO —— 通过遍历所有的区块来找到所有未被花费的交易输出.

  2. UTXOSet.reindex —— 调用上面 findUTXO 方法,然后将查询结果存储在数据库中。也即需要进行缓存的地方。

  3. UTXOSet.findSpendableOutputs —— 与 Blockchain.findSpendableOutputs 类似,区别在于会使用 UTXO 池。

  4. UTXOSet.findUTXO —— 与Blockchain.findUTXO 类似,区别在于会使用 UTXO 池。

  5. Blockchain.findTransaction —— 逻辑保持不变。

这样,两个使用最频繁的方法将从现在开始使用缓存!

接下来,我们个通过代码实现一下:

首先,我们在cldy.hanru.blockchain.transaction包下再创建一个java文件,命名为:UTXOSet.java。添加一个类:

/** * 未被花费的交易输出池 * @author hanru */@NoArgsConstructor@AllArgsConstructor@Slf4jpublic class UTXOSet {

private Blockchain blockchain;

}

我们将会使用一个单一数据库,但是我们会将 UTXO 集从存储在不同的 bucket 中。因此,UTXOSet 跟 Blockchain 一起。

因为我们需要有个bucket来存储所有的UTXO,所以我们修改RocksDBUtils.java文件,添加对应的存储操作方法。

首先先定义存储UTXO的bucket

/** * 链状态桶Key */private static final String CHAINSTATE_BUCKET_KEY = "chainstate";/** * chainstate buckets */@Getterprivate Map<String, byte[]> chainstateBucket;

接下来定义一些工具方法,initChainStateBucket()方法用于初始化bucket,并在该类的构造函数中调用:

/** * 初始化 blocks 数据桶 */private void initChainStateBucket() {try {byte[] chainstateBucketKey = SerializeUtils.serialize(CHAINSTATE_BUCKET_KEY);byte[] chainstateBucketBytes = db.get(chainstateBucketKey);if (chainstateBucketBytes != null) { chainstateBucket = (Map) SerializeUtils.deserialize(chainstateBucketBytes); } else { chainstateBucket = new HashMap(); db.put(chainstateBucketKey, SerializeUtils.serialize(chainstateBucket)); } } catch (RocksDBException e) { log.error("Fail to init chainstate bucket ! ", e);throw new RuntimeException("Fail to init chainstate bucket ! ", e); } }

cleanChainStateBucket()方法用于清空bucket:

/** * 清空chainstate bucket */public void cleanChainStateBucket() {try { chainstateBucket.clear(); } catch (Exception e) {log.error("Fail to clear chainstate bucket ! ", e);throw new RuntimeException("Fail to clear chainstate bucket ! ", e); }}

最后定义3个方法,用于表示添加、获取、删除UTXO:

/** * 保存UTXO数据 * * @param key 交易ID * @param utxos UTXOs */public void putUTXOs(String key, TXOutput[] utxos) {try { chainstateBucket.put(key, SerializeUtils.serialize(utxos)); db.put(SerializeUtils.serialize(CHAINSTATE_BUCKET_KEY), SerializeUtils.serialize(chainstateBucket)); } catch (Exception e) { log.error("Fail to put UTXOs into chainstate bucket ! key=" + key, e);throw new RuntimeException("Fail to put UTXOs into chainstate bucket ! key=" + key, e); } }

/** * 查询UTXO数据 * * @param key 交易ID */public TXOutput[] getUTXOs(String key) {byte[] utxosByte = chainstateBucket.get(key);if (utxosByte != null) {return (TXOutput[]) SerializeUtils.deserialize(utxosByte); }return null; }

/** * 删除 UTXO 数据 * * @param key 交易ID */public void deleteUTXOs(String key) {try { chainstateBucket.remove(key); db.put(SerializeUtils.serialize(CHAINSTATE_BUCKET_KEY), SerializeUtils.serialize(chainstateBucket)); } catch (Exception e) { log.error("Fail to delete UTXOs by key ! key=" + key, e);throw new RuntimeException("Fail to delete UTXOs by key ! key=" + key, e); } }

工具类准备好后,我们在UTXOSet.java中,继续定义一个reIndex()方法:

/** * 重建 UTXO 池索引 */@Synchronizedpublic void reIndex() {log.info("Start to reIndex UTXO set !"); RocksDBUtils.getInstance().cleanChainStateBucket(); Map allUTXOs = blockchain.findAllUTXOs();for (Map.Entry entry : allUTXOs.entrySet()) { RocksDBUtils.getInstance().putUTXOs(entry.getKey(), entry.getValue()); }log.info("ReIndex UTXO set finished ! ");}

这个方法初始化了 UTXO 集。首先,先清除之前的bucket。然后从区块链中获取所有的未花费输出,最终将输出保存到 bucket 中。该方法也用于重置UTXO集。

Blockchain.java中添加方法,用于查询所有的未花费输出:

/** * 查找所有的 unspent transaction outputs * * @return */public Map findAllUTXOs() { Mapint[]> allSpentTXOs = this.getAllSpentTXOs(); Map allUTXOs = new HashMap();// 再次遍历所有区块中的交易输出for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { Block block = blockchainIterator.next();for (Transaction transaction : block.getTransactions()) { String txId = Hex.encodeHexString(transaction.getTxId());int[] spentOutIndexArray = allSpentTXOs.get(txId); TXOutput[] txOutputs = transaction.getOutputs();for (int outIndex = 0; outIndex < txOutputs.length; outIndex++) {if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) {continue; } TXOutput[] UTXOArray = allUTXOs.get(txId);if (UTXOArray == null) { UTXOArray = new TXOutput[]{txOutputs[outIndex]}; } else { UTXOArray = ArrayUtils.add(UTXOArray, txOutputs[outIndex]); } allUTXOs.put(txId, UTXOArray); } } }return allUTXOs; }

接下来,修改CLI.java文件,修改createBlockchain()方法代码如下:

/** * 创建区块链 * * @param address */private void createBlockchain(String address) {

 Blockchain blockchain = Blockchain.createBlockchain(address); UTXOSet utxoSet = new UTXOSet(blockchain); utxoSet.reIndex();log.info("Done ! "); }

当创建创世区块后,就会立刻进行初始化UTXO集。目前,即使这里看起来有点“杀鸡用牛刀”,因为一条链开始的时候,只有一个块,里面只有一笔交易。

因为创建了创世区块,有CoinBase交易的10个Token,这是一个未花费的TxOutput,找到之后可以存储到UTXO集中。

4.3 优化查询余额

有了UTXO集,我们想要查询余额,可以不用遍历整个区块链的所有区块,而是直接查找UTXO集,找出对应地址的utxo,进行累加即可。

接下来,我们在UTXOSet.java中,添加findUTXOs(),用于查找指定账户的所有的utxo,代码如下:

/** * 查找钱包地址对应的所有UTXO * * @param pubKeyHash 钱包公钥Hash * @return */public TXOutput[] findUTXOs(byte[] pubKeyHash) { TXOutput[] utxos = {}; Mapbyte[]> chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket();if (chainstateBucket.isEmpty()) {return utxos; }for (byte[] value : chainstateBucket.values()) { TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(value);for (TXOutput txOutput : txOutputs) {if (txOutput.isLockedWithKey(pubKeyHash)) { utxos = ArrayUtils.add(utxos, txOutput); } } }return utxos; }

接下来修改,修改CLI.java文件,不再通过blockchain对象调用原来的查询方法了,改用utxoSet对象进行查询余额,代码如下:

/** * 查询钱包余额 * * @param address 钱包地址 */private void getBalance(String address) throws Exception {

// 检查钱包地址是否合法try { Base58Check.base58ToBytes(address); } catch (Exception e) {throw new Exception("ERROR: invalid wallet address"); } Blockchain blockchain = Blockchain.createBlockchain(address);// 得到公钥Hash值byte[] versionedPayload = Base58Check.base58ToBytes(address);byte[] pubKeyHash = Arrays.copyOfRange(versionedPayload, 1, versionedPayload.length);

 UTXOSet utxoSet = new UTXOSet(blockchain); TXOutput[] txOutputs = utxoSet.findUTXOs(pubKeyHash);int balance = 0;if (txOutputs != null && txOutputs.length > 0) {for (TXOutput txOutput : txOutputs) { balance += txOutput.getValue(); } } System.out.printf("Balance of '%s': %d\n", address, balance); }

接下来,我们测试一下,先进行一次转账(转账我们还没有使用UTXO集优化),然后再查询余额。在终端中输入以下命令:

# 首先重新编译程序hanru:part8_Transaction2 ruby$ mvn package# 创建新的钱包地址hanru:part8_Transaction2 ruby$ ./blockchain.sh createwallet# 转账,19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2账户因为之前的转账操作,有4个Tokenhanru:part8_Transaction2 ruby$ ./blockchain.sh send -from 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2 -to 17mW1XamRdFZBsnmfh3DxbC5pW1AttJznn -amount 3# 查询余额hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 17mW1XamRdFZBsnmfh3DxbC5pW1AttJznn

运行结果如下:

5. 总结

本章节中,我们并没有在项目中新增功能,只是做了一些优化。首先加入了奖励Reward机制。虽然程序中我们实现的比较建议,仅仅是给发起转账的人奖励10个Token,(如果一次转账多笔交易,只奖励给第一个转账人)。然后我们引入了UTXO集,进行项目代码优化。UTXO集的原理,就是我们将所有区块的未花费的utxo,单独存储到一个bucket中。无论是进行余额查询,还是转账操作,都无需遍历查询所有的区块,查询所有的交易去找未花费utxo了,只需要查询UTXO集即可。如果是转账操作,转账后需要及时更新UTXO集。

修改linq结果集_UTXO集优化相关推荐

  1. 阿里巴巴云原生 etcd 服务集群管控优化实践

    作者 | 陈星宇(宇慕) 来源 | 阿里巴巴云原生公众号 背景 Kubernetes 采用 etcd 存储其内部核心元数据信息.经过这些年的发展,尤其是伴随着这两年云原生的快速发展,Kubernete ...

  2. DL之NN:NN算法(本地数据集50000张训练集图片)进阶优化之三种参数改进,进一步提高手写数字图片识别的准确率

    DL之NN:NN算法(本地数据集50000张训练集图片)进阶优化之三种参数改进,进一步提高手写数字图片识别的准确率 导读 上一篇文章,比较了三种算法实现对手写数字识别,其中,SVM和神经网络算法表现非 ...

  3. 南华大学计算机学院吴取劲,一种基于图深度优先搜索的基本路径集自动生成优化算法-南华大学学报.PDF...

    26 3 ( ) Vol. 26 No. 3 第 卷第 期 南华大学学报 自然科学版 2012 9 Journal of University of South China (Science and ...

  4. UI UX 小提示合集 -- 第一集

    UI & UX 小提示合集 -- 第一集 17个小提示让你的设计瞬间升级 我了解设计一个既漂亮又实用的界面的过程 - 通常较长,而且需要反复修改.大多数设计师都有类似的经历. 不过,多年的经验 ...

  5. 吴恩达神经网络和深度学习-学习笔记-21-何时该改变开发集+测试集或指标

    需要我们改变开发集+测试集或指标的情况,是现有的指标或开发测试集并不能帮助我们选择出最好的模型! 如果你的评估指标,无法正确评估好算法的排名,那么就需要花时间定义一个新的评估指标. (加权来排除某一类 ...

  6. MongoDB 分片(sharding)+副本集(replSet)集群搭建

    文章目录 MongoDB安装 Windows平台安装 1.下载 2.安装 3.启动MongoDB服务 4.进入MongoDB后台 Linux平台安装MongoDB 1.下载 2.安装 3.创建数据库目 ...

  7. 什么是集群,集群的概念介绍

    集群术语须知 服务硬件:指提供计算服务的硬件,比如 PC 机.PC 服务器. 服务实体:服务实体通常指服务软体和服务硬体. 节点(node):运行 Heartbeat 进程的一个独立主机称为节点,节点 ...

  8. Pytorch---- CIFAR10实战(训练集+测试集+验证集)完整版,逐行注释-----学习笔记

    文章目录 CIFAR10数据集准备.加载 搭建神经网络 损失函数和优化器 训练集 测试集 关于argmax: 使用tensorboard可视化训练过程. 完整代码(训练集+测试集): 程序结果: 验证 ...

  9. 十折交叉验证10-fold cross validation, 数据集划分 训练集 验证集 测试集

    机器学习 数据挖掘 数据集划分 训练集 验证集 测试集 Q:如何将数据集划分为测试数据集和训练数据集? A:three ways: 1.像sklearn一样,提供一个将数据集切分成训练集和测试集的函数 ...

最新文章

  1. 程序员哀叹外国同事对35岁现象感到震惊,在国外40岁还写代码
  2. nginx的函数调用
  3. 如何使用 C# 压缩单个文件?
  4. windows模拟微信小程序_Windows 版微信新版本内测!小程序可以直接添加到电脑桌面了...
  5. 【原创】昆虫棋离线复盘工具v1.5 更新(BoardSpace.net hive games reviewer)
  6. linux中vim如何替换字符串,vim中替换字符串的方法有哪些
  7. 火狐扩展教程_Firefox扩展模板
  8. Unity VR开发教程 OpenXR+XR Interaction Toolkit 2.1.1 (一) 安装和配置
  9. c语言函数定义四要素,C语言函数
  10. JFinal+Quartz动态任务调度控制台
  11. [java 新手练习1]5x5横排竖排方阵代码(java)
  12. 抖音带货平台怎么收费?抖音带货费用有哪些
  13. Win10中安装Oracle11g
  14. 操作系统学习笔记(5. 文件管理)
  15. 阿里云服务器10M带宽实际速度能达到多少?
  16. 发那科数控车ftp联网
  17. EMP微前端-Vue和React项目互相调用
  18. 【数据恢复软件】来,认识一下
  19. Java设计模式-策略模式(一)
  20. unity中声音大小控制物体行为(音量的大小)

热门文章

  1. Python类的静态属性、类方法、静态方法
  2. Linux最小体积mysql安装_Linux下安装MySQL以及一些小坑
  3. linux ubuntu16.04 编译opencv教程(没编过,有时间再弄,推流不用opencv也行的)
  4. 结构化道路上车辆自动驾驶中的雷达类型及安装位置
  5. python中三个双引号 的作用是什么?1、多行注释 2、定义多行字符串(代替转义字符换行符 \n)
  6. python 多进程 multiprocessing.Queue()报错:The freeze_support() line can be omitted if the program
  7. C++ 栈和堆上建立对象的区别
  8. python的yield和yield from
  9. Java中的主类概念以及public static void main方法的分析
  10. Netty原理四:客户端Bootstrap启动连接时做了些什么?