修改linq结果集_UTXO集优化
在这个系列文章的一开始,我们就提到了,区块链是一个分布式数据库。不过在之前的文章中,我们选择性地跳过了“分布式”这个部分,而是将注意力都放到了“数据库”部分。到目前为止,我们几乎已经实现了一个区块链数据库的所有元素(基本原型,工作量证明,持久化,命令行接口,交易,地址,数字签名等)。今天,我们将会分析之前跳过的一些机制。而在下一篇文章中,我们将会开始讨论区块链的分布式特性。
1. 课程目标
知道什么是奖励
知道什么是UTXO集
学会UTXO集优化的原理
项目中修改代码实现UTXO集查询余额
项目中修改代码实现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
运行结果如下:
说明:
比如创世区块中1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt地址有10个Token,
转账给19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2地址4个Token,
那么1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt的余额是16(转账找零6个,加上得到奖励10个),
19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2的余额是4。
4.2 UTXO 集
在持久化的章节中,我们研究了 Bitcoin Core 是如何在一个数据库中存储块的,并且了解到区块被存储在 blocks
数据库,交易输出被存储在 chainstate
数据库。会回顾一下 chainstate
的结构:
c
+ 32 字节的交易哈希 -> 该笔交易的未花费交易输出记录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 池我们需要做哪些事情。当前,有下列方法被用于查找交易信息:
Blockchain.getAllSpentTXOs —— 查询所有已被花费的交易输出。它需要遍历区块链中所有区块中交易信息。
Blockchain.findUnspentTransactions —— 查询包含未被花费的交易输出的交易信息。它也需要遍历区块链中所有区块中交易信息。
Blockchain.findSpendableOutputs —— 该方法用于新的交易创建之时。它需要找到足够多的交易输出,以满足所需支付的金额。需要调用 Blockchain.findUnspentTransactions 方法。
Blockchain.findUTXO —— 查询钱包地址所对应的所有未花费交易输出,然后用于计算钱包余额。需要调用
Blockchain.findUnspentTransactions 方法。
Blockchain.findTransaction —— 通过交易ID查询交易信息。它需要遍历所有的区块直到找到交易信息为止。
如你所见,上面这些方法都需要去遍历数据库中的所有区块。由于UTXOs池只存储未被花费的交易输出,而不会存储所有的交易信息,因此我们不会对有 Blockchain.findTransaction 进行优化。
那么,我们需要下列这些方法:
Blockchain.findUTXO —— 通过遍历所有的区块来找到所有未被花费的交易输出.
UTXOSet.reindex —— 调用上面 findUTXO 方法,然后将查询结果存储在数据库中。也即需要进行缓存的地方。
UTXOSet.findSpendableOutputs —— 与 Blockchain.findSpendableOutputs 类似,区别在于会使用 UTXO 池。
UTXOSet.findUTXO —— 与Blockchain.findUTXO 类似,区别在于会使用 UTXO 池。
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集优化相关推荐
- 阿里巴巴云原生 etcd 服务集群管控优化实践
作者 | 陈星宇(宇慕) 来源 | 阿里巴巴云原生公众号 背景 Kubernetes 采用 etcd 存储其内部核心元数据信息.经过这些年的发展,尤其是伴随着这两年云原生的快速发展,Kubernete ...
- DL之NN:NN算法(本地数据集50000张训练集图片)进阶优化之三种参数改进,进一步提高手写数字图片识别的准确率
DL之NN:NN算法(本地数据集50000张训练集图片)进阶优化之三种参数改进,进一步提高手写数字图片识别的准确率 导读 上一篇文章,比较了三种算法实现对手写数字识别,其中,SVM和神经网络算法表现非 ...
- 南华大学计算机学院吴取劲,一种基于图深度优先搜索的基本路径集自动生成优化算法-南华大学学报.PDF...
26 3 ( ) Vol. 26 No. 3 第 卷第 期 南华大学学报 自然科学版 2012 9 Journal of University of South China (Science and ...
- UI UX 小提示合集 -- 第一集
UI & UX 小提示合集 -- 第一集 17个小提示让你的设计瞬间升级 我了解设计一个既漂亮又实用的界面的过程 - 通常较长,而且需要反复修改.大多数设计师都有类似的经历. 不过,多年的经验 ...
- 吴恩达神经网络和深度学习-学习笔记-21-何时该改变开发集+测试集或指标
需要我们改变开发集+测试集或指标的情况,是现有的指标或开发测试集并不能帮助我们选择出最好的模型! 如果你的评估指标,无法正确评估好算法的排名,那么就需要花时间定义一个新的评估指标. (加权来排除某一类 ...
- MongoDB 分片(sharding)+副本集(replSet)集群搭建
文章目录 MongoDB安装 Windows平台安装 1.下载 2.安装 3.启动MongoDB服务 4.进入MongoDB后台 Linux平台安装MongoDB 1.下载 2.安装 3.创建数据库目 ...
- 什么是集群,集群的概念介绍
集群术语须知 服务硬件:指提供计算服务的硬件,比如 PC 机.PC 服务器. 服务实体:服务实体通常指服务软体和服务硬体. 节点(node):运行 Heartbeat 进程的一个独立主机称为节点,节点 ...
- Pytorch---- CIFAR10实战(训练集+测试集+验证集)完整版,逐行注释-----学习笔记
文章目录 CIFAR10数据集准备.加载 搭建神经网络 损失函数和优化器 训练集 测试集 关于argmax: 使用tensorboard可视化训练过程. 完整代码(训练集+测试集): 程序结果: 验证 ...
- 十折交叉验证10-fold cross validation, 数据集划分 训练集 验证集 测试集
机器学习 数据挖掘 数据集划分 训练集 验证集 测试集 Q:如何将数据集划分为测试数据集和训练数据集? A:three ways: 1.像sklearn一样,提供一个将数据集切分成训练集和测试集的函数 ...
最新文章
- 程序员哀叹外国同事对35岁现象感到震惊,在国外40岁还写代码
- nginx的函数调用
- 如何使用 C# 压缩单个文件?
- windows模拟微信小程序_Windows 版微信新版本内测!小程序可以直接添加到电脑桌面了...
- 【原创】昆虫棋离线复盘工具v1.5 更新(BoardSpace.net hive games reviewer)
- linux中vim如何替换字符串,vim中替换字符串的方法有哪些
- 火狐扩展教程_Firefox扩展模板
- Unity VR开发教程 OpenXR+XR Interaction Toolkit 2.1.1 (一) 安装和配置
- c语言函数定义四要素,C语言函数
- JFinal+Quartz动态任务调度控制台
- [java 新手练习1]5x5横排竖排方阵代码(java)
- 抖音带货平台怎么收费?抖音带货费用有哪些
- Win10中安装Oracle11g
- 操作系统学习笔记(5. 文件管理)
- 阿里云服务器10M带宽实际速度能达到多少?
- 发那科数控车ftp联网
- EMP微前端-Vue和React项目互相调用
- 【数据恢复软件】来,认识一下
- Java设计模式-策略模式(一)
- unity中声音大小控制物体行为(音量的大小)
热门文章
- Python类的静态属性、类方法、静态方法
- Linux最小体积mysql安装_Linux下安装MySQL以及一些小坑
- linux ubuntu16.04 编译opencv教程(没编过,有时间再弄,推流不用opencv也行的)
- 结构化道路上车辆自动驾驶中的雷达类型及安装位置
- python中三个双引号 的作用是什么?1、多行注释 2、定义多行字符串(代替转义字符换行符 \n)
- python 多进程 multiprocessing.Queue()报错:The freeze_support() line can be omitted if the program
- C++ 栈和堆上建立对象的区别
- python的yield和yield from
- Java中的主类概念以及public static void main方法的分析
- Netty原理四:客户端Bootstrap启动连接时做了些什么?