修改linq结果集_UTXO集优化
在這個(gè)系列文章的一開始,我們就提到了,區(qū)塊鏈?zhǔn)且粋€(gè)分布式數(shù)據(jù)庫。不過在之前的文章中,我們選擇性地跳過了“分布式”這個(gè)部分,而是將注意力都放到了“數(shù)據(jù)庫”部分。到目前為止,我們幾乎已經(jīng)實(shí)現(xiàn)了一個(gè)區(qū)塊鏈數(shù)據(jù)庫的所有元素(基本原型,工作量證明,持久化,命令行接口,交易,地址,數(shù)字簽名等)。今天,我們將會分析之前跳過的一些機(jī)制。而在下一篇文章中,我們將會開始討論區(qū)塊鏈的分布式特性。
1. 課程目標(biāo)
知道什么是獎(jiǎng)勵(lì)
知道什么是UTXO集
學(xué)會UTXO集優(yōu)化的原理
項(xiàng)目中修改代碼實(shí)現(xiàn)UTXO集查詢余額
項(xiàng)目中修改代碼實(shí)現(xiàn)UTXO集進(jìn)行轉(zhuǎn)賬交易
2. 項(xiàng)目代碼及效果展示
2.1 項(xiàng)目代碼結(jié)構(gòu)
2.2 項(xiàng)目運(yùn)行結(jié)果
創(chuàng)建錢包地址,創(chuàng)建創(chuàng)世區(qū)塊和使用UTXOSet優(yōu)化后的轉(zhuǎn)賬,查詢余額效果圖如下:
3. 創(chuàng)建項(xiàng)目
3.1 創(chuàng)建工程
打開IntelliJ IDEA的工作空間,將上一個(gè)項(xiàng)目代碼目錄part7_Signature,復(fù)制為part8_Transaction2。
然后打開IntelliJ IDEA開發(fā)工具。
打開工程:part8_Transaction2,并刪除target目錄。然后進(jìn)行以下修改:
step1:先將項(xiàng)目重新命名為:part8_Transaction2。step2:修改pom.xml配置文件。
改為:<artifactId>part8_Transaction2artifactId>標(biāo)簽
改為:<name>part8_Transaction2 Maven Webappname>
3.2 代碼實(shí)現(xiàn)
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()構(gòu)造函數(shù)
修改完后代碼如下:
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;
/**
* 數(shù)據(jù)庫存儲的工具類
* @author hanru
*/
@Slf4j
public class RocksDBUtils {
/**
* 區(qū)塊鏈數(shù)據(jù)文件
*/
private static final String DB_FILE = "blockchain.db";
/**
* 區(qū)塊桶前綴
*/
private static final String BLOCKS_BUCKET_KEY = "blocks";
/**
* 鏈狀態(tài)桶Key
*/
private static final String CHAINSTATE_BUCKET_KEY = "chainstate";
/**
* 最新一個(gè)區(qū)塊的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;/**
* 打開數(shù)據(jù)庫
*/private void openDB() {try {
db = RocksDB.open(DB_FILE);
} catch (RocksDBException e) {throw new RuntimeException("打開數(shù)據(jù)庫失敗。。 ! ", e);
}
}/**
* 初始化 blocks 數(shù)據(jù)桶
*/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);
}
}/**
* 保存區(qū)塊
*
* @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("存儲區(qū)塊失敗。。 ", e);
}
}/**
* 查詢區(qū)塊
*
* @param blockHash
* @return
*/public Block getBlock(String blockHash) {return (Block) SerializeUtils.deserialize(blocksBucket.get(blockHash));
}/**
* 保存最新一個(gè)區(qū)塊的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("數(shù)據(jù)庫存儲最新區(qū)塊hash失敗。。 ", e);
}
}/**
* 查詢最新一個(gè)區(qū)塊的Hash值
*
* @return
*/public String getLastBlockHash() {byte[] lastBlockHashBytes = blocksBucket.get(LAST_BLOCK_KEY);if (lastBlockHashBytes != null) {return (String) SerializeUtils.deserialize(lastBlockHashBytes);
}return "";
}/**
* 關(guān)閉數(shù)據(jù)庫
*/public void closeDB() {try {
db.close();
} catch (Exception e) {throw new RuntimeException("關(guān)閉數(shù)據(jù)庫失敗。。 ", e);
}
}/**
* chainstate buckets
*/
@Getterprivate Mapbyte[]> chainstateBucket;/**
* 初始化 blocks 數(shù)據(jù)桶
*/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數(shù)據(jù)
*
* @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數(shù)據(jù)
*
* @param key 交易ID
*/public TXOutput[] getUTXOs(String key) {byte[] utxosByte = chainstateBucket.get(key);if (utxosByte != null) {return (TXOutput[]) SerializeUtils.deserialize(utxosByte);
}return null;
}/**
* 刪除 UTXO 數(shù)據(jù)
*
* @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 創(chuàng)建UTXOSet.java文件
打開cldy.hanru.blockchain.transaction包,創(chuàng)建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;
/**
* 未被花費(fèi)的交易輸出池
* @author hanru
*/
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class UTXOSet {
private Blockchain blockchain;
/**
* 重建 UTXO 池索引
*/
@Synchronized
public 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 ! ");
}/**
* 查找錢包地址對應(yīng)的所有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;
}/**
* 尋找能夠花費(fèi)的交易
*
* @param pubKeyHash 錢包公鑰Hash
* @param amount 花費(fèi)金額
*/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池
* 當(dāng)一個(gè)新的區(qū)塊產(chǎn)生時(shí),需要去做兩件事情:
* 1)從UTXO池中移除花費(fèi)掉了的交易輸出;
* 2)保存新的未花費(fèi)交易輸出;
*
* @param tipBlock 最新的區(qū)塊
*/@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()) {// 根據(jù)交易輸入排查出剩余未被使用的交易輸出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;
/**
* 區(qū)塊鏈
*
* @author hanru
*/
@Data
@AllArgsConstructor
public class Blockchain {
/**
* 最后一個(gè)區(qū)塊的hash
*/
private String lastBlockHash;
/**
* 創(chuàng)建區(qū)塊鏈,createBlockchain
*
* @param address
* @return
*/
public static Blockchain createBlockchain(String address) {
String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();
if (StringUtils.isBlank(lastBlockHash)) {
//對應(yīng)的bucket不存在,說明是第一次獲取區(qū)塊鏈實(shí)例
// 創(chuàng)建 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);
}
/**
* 根據(jù)block,添加區(qū)塊
*
* @param block
*/
public void addBlock(Block block) {
RocksDBUtils.getInstance().putLastBlockHash(block.getHash());
RocksDBUtils.getInstance().putBlock(block);
this.lastBlockHash = block.getHash();
}
/**
* 區(qū)塊鏈迭代器:內(nèi)部類
*/
public class BlockchainIterator {
/**
* 當(dāng)前區(qū)塊的hash
*/
private String currentBlockHash;
/**
* 構(gòu)造函數(shù)
*
* @param currentBlockHash
*/
public BlockchainIterator(String currentBlockHash) {
this.currentBlockHash = currentBlockHash;
}
/**
* 判斷是否有下一個(gè)區(qū)塊
*
* @return
*/
public boolean hashNext() {
if (ByteUtils.ZERO_HASH.equals(currentBlockHash)) {
return false;
}
Block lastBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash);
if (lastBlock == null) {
return false;
}
// 如果是創(chuàng)世區(qū)塊
if (ByteUtils.ZERO_HASH.equals(lastBlock.getPrevBlockHash())) {
return true;
}
return RocksDBUtils.getInstance().getBlock(lastBlock.getPrevBlockHash()) != null;
}
/**
* 迭代獲取區(qū)塊
*
* @return
*/
public Block next() {
Block currentBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash);
if (currentBlock != null) {
this.currentBlockHash = currentBlock.getPrevBlockHash();
return currentBlock;
}
return null;
}
}
/**
* 添加方法,用于獲取迭代器實(shí)例
*
* @return
*/
public BlockchainIterator getBlockchainIterator() {
return new BlockchainIterator(lastBlockHash);
}
/**
* 打包交易,進(jìn)行挖礦
*
* @param transactions
*/
public Block mineBlock(List transactions) throws Exception {
// 挖礦前,先驗(yàn)證交易記錄
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;
}
/**
* 依據(jù)交易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 從恢復(fù)區(qū)塊鏈數(shù)據(jù)
*
* @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);
}
/**
* 進(jìn)行交易簽名
*
* @param tx 交易數(shù)據(jù)
* @param privateKey 私鑰
*/
public void signTransaction(Transaction tx, BCECPrivateKey privateKey) throws Exception {
// 先來找到這筆新的交易中,交易輸入所引用的前面的多筆交易的數(shù)據(jù)
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);
}/**
* 交易簽名驗(yàn)證
*
* @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 ! ");
}
}/**
* 從交易輸入中查詢區(qū)塊鏈中所有已被花費(fèi)了的交易輸出
*
* @return 交易ID以及對應(yīng)的交易輸出下標(biāo)地址
*/private Mapint[]> getAllSpentTXOs() {// 定義TxId ——> spentOutIndex[],存儲交易ID與已被花費(fèi)的交易輸出數(shù)組索引值
Mapint[]> spentTXOs = new HashMap<>();for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) {
Block block = blockchainIterator.next();for (Transaction transaction : block.getTransactions()) {// 如果是 coinbase 交易,直接跳過,因?yàn)樗淮嬖谝们耙粋€(gè)區(qū)塊的交易輸出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<>();// 再次遍歷所有區(qū)塊中的交易輸出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:添加時(shí)間戳字段
step2:修改newUTXOTransaction方法,從utxoSet中查找轉(zhuǎn)賬要使用的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
@NoArgsConstructor
public class Transaction {
private static final int SUBSIDY = 10;
/**
* 交易的Hash
*/
private byte[] txId;
/**
* 交易輸入
*/
private TXInput[] inputs;
/**
* 交易輸出
*/
private TXOutput[] outputs;
/**
* 創(chuàng)建日期
*/
private long createTime;
/**
* 設(shè)置交易ID
*/
private void setTxId() {
this.setTxId(DigestUtils.sha256(SerializeUtils.serialize(this)));
}
/**
* 要簽名的數(shù)據(jù)
* @return
*/
public byte[] getData() {
// 使用序列化的方式對Transaction對象進(jìn)行深度復(fù)制
byte[] serializeBytes = SerializeUtils.serialize(this);
Transaction copyTx = (Transaction) SerializeUtils.deserialize(serializeBytes);
copyTx.setTxId(new byte[]{});
return DigestUtils.sha256(SerializeUtils.serialize(copyTx));
}
/**
* 創(chuàng)建CoinBase交易
*
* @param to 收賬的錢包地址
* @param data 解鎖腳本數(shù)據(jù)
* @return
*/
public static Transaction newCoinbaseTX(String to, String data) {
if (StringUtils.isBlank(data)) {
data = String.format("Reward to '%s'", to);
}
// 創(chuàng)建交易輸入
TXInput txInput = new TXInput(new byte[]{}, -1, null, data.getBytes());
// 創(chuàng)建交易輸出
TXOutput txOutput = TXOutput.newTXOutput(SUBSIDY, to);
// 創(chuàng)建交易
Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput}, System.currentTimeMillis());
// 設(shè)置交易ID
tx.setTxId();
return tx;
}
/**
* 從 from 向 to 支付一定的 amount 的金額
*
* @param from 支付錢包地址
* @param to 收款錢包地址
* @param amount 交易金額
* @param blockchain 區(qū)塊鏈
* @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();// 進(jìn)行交易簽名
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;
}/**
* 創(chuàng)建用于簽名的交易數(shù)據(jù)副本,交易輸入的 signature 和 pubKey 需要設(shè)置為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 交易信息不需要簽名,因?yàn)樗淮嬖诮灰纵斎胄畔f (this.isCoinbase()) {return;
}// 再次驗(yàn)證一下交易信息中的交易輸入是否正確,也就是能否查找對應(yīng)的交易數(shù)據(jù)for (TXInput txInput : this.getInputs()) {if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) {throw new Exception("ERROR: Previous transaction is not correct");
}
}// 創(chuàng)建用于簽名的交易信息的副本
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對應(yīng)的交易數(shù)據(jù)
Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId()));// 獲取交易輸入所對應(yīng)的上一筆交易中的交易輸出
TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()];
txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
txInputCopy.setSignature(null);// 得到要簽名的數(shù)據(jù)byte[] signData = txCopy.getData();
txInputCopy.setPubKey(null);// 對整個(gè)交易信息僅進(jìn)行簽名
ecdsaSign.update(signData);byte[] signature = ecdsaSign.sign();// 將整個(gè)交易數(shù)據(jù)的簽名賦值給交易輸入,因?yàn)榻灰纵斎胄枰麄€(gè)交易信息的簽名// 注意是將得到的簽名賦值給原交易信息中的交易輸入this.getInputs()[i].setSignature(signature);
}
}/**
* 驗(yàn)證交易信息
*
* @param prevTxMap 前面多筆交易集合
* @return
*/public boolean verify(Map prevTxMap) throws Exception {// coinbase 交易信息不需要簽名,也就無需驗(yàn)證if (this.isCoinbase()) {return true;
}// 再次驗(yàn)證一下交易信息中的交易輸入是否正確,也就是能否查找對應(yīng)的交易數(shù)據(jù)for (TXInput txInput : this.getInputs()) {if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) {throw new Exception("ERROR: Previous transaction is not correct");
}
}// 創(chuàng)建用于簽名驗(yàn)證的交易信息的副本
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對應(yīng)的交易數(shù)據(jù)
Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId()));// 獲取交易輸入所對應(yīng)的上一筆交易中的交易輸出
TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()];
TXInput txInputCopy = txCopy.getInputs()[i];
txInputCopy.setSignature(null);
txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());// 得到要簽名的數(shù)據(jù)byte[] signData = txCopy.getData();
txInputCopy.setPubKey(null);// 使用橢圓曲線 x,y 點(diǎn)去生成公鑰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
*/
@Slf4j
public 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);
}
/**
* 驗(yàn)證入?yún)?br /> *
* @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();
}
}
/**
* 創(chuàng)建區(qū)塊鏈
*
* @param address
*/
private void createBlockchain(String address) {
Blockchain blockchain = Blockchain.createBlockchain(address);
UTXOSet utxoSet = new UTXOSet(blockchain);
utxoSet.reIndex();
log.info("Done ! ");
}
/**
* 打印出區(qū)塊鏈中的所有區(qū)塊
*/
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() + "個(gè)區(qū)塊信息:");
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);
}
/**
* 轉(zhuǎn)賬
*
* @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);
// 獎(jiǎng)勵(lì)
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!");
}/**
* 創(chuàng)建錢包
*
* @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腳本文件,修改后內(nèi)容如下:
#!/bin/bashset -e
# Check if the jar has been built.
if [ ! -e target/part8_Transaction2-jar-with-dependencies.jar ]; then
echo "Compiling blockchain project to a JAR"
mvn package -DskipTests
fi
java -jar target/part8_Transaction2-jar-with-dependencies.jar "$@"
4. 優(yōu)化內(nèi)容講解
4.1 獎(jiǎng)勵(lì)Reward
在上一篇文章中,我們略過的一個(gè)小細(xì)節(jié)是挖礦獎(jiǎng)勵(lì)。現(xiàn)在,我們已經(jīng)可以來完善這個(gè)細(xì)節(jié)了。
挖礦獎(jiǎng)勵(lì),實(shí)際上就是一筆coinbase?交易。當(dāng)一個(gè)挖礦節(jié)點(diǎn)開始挖出一個(gè)新塊時(shí),它會將交易從隊(duì)列中取出,并在前面附加一筆coinbase交易。coinbase?交易只有一個(gè)輸出,里面包含了礦工的公鑰哈希。
實(shí)現(xiàn)獎(jiǎng)勵(lì),非常簡單,更新CLI類即可,修改?send()?方法:
/*** 轉(zhuǎn)賬
*
* @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);
// 獎(jiǎng)勵(lì)
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!");
}
現(xiàn)在我們的邏輯調(diào)整為 ,每次轉(zhuǎn)賬都會給與獎(jiǎng)勵(lì),而獎(jiǎng)勵(lì)通過coinbase交易實(shí)現(xiàn),為了讓這些coinbase交易的交易ID不同,我們需要在Transaction類添加時(shí)間字段:
public class Transaction {private static final int SUBSIDY = 10;
...
/**
* 創(chuàng)建日期
*/
private long createTime;
}
接下來就需要修改創(chuàng)建coinbase交易和創(chuàng)建轉(zhuǎn)賬交易的方法,添加創(chuàng)建日期:
/*** 創(chuàng)建CoinBase交易
*
* @param to 收賬的錢包地址
* @param data 解鎖腳本數(shù)據(jù)
* @return
*/
public static Transaction newCoinbaseTX(String to, String data) {
if (StringUtils.isBlank(data)) {
data = String.format("Reward to '%s'", to);
}
// 創(chuàng)建交易輸入
TXInput txInput = new TXInput(new byte[]{}, -1, null, data.getBytes());
// 創(chuàng)建交易輸出
TXOutput txOutput = TXOutput.newTXOutput(SUBSIDY, to);
// 創(chuàng)建交易
Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput}, System.currentTimeMillis());
// 設(shè)置交易ID
tx.setTxId();
return tx;
}
以及:
/*** 從 from 向 to 支付一定的 amount 的金額
*
* @param from 支付錢包地址
* @param to 收款錢包地址
* @param amount 交易金額
* @param blockchain 區(qū)塊鏈
* @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();// 進(jìn)行交易簽名
blockchain.signTransaction(newTx, senderWallet.getPrivateKey());return newTx;
}
在我們的實(shí)現(xiàn)中,創(chuàng)建交易的第一個(gè)人同時(shí)挖出了新塊,所以會得到一筆獎(jiǎng)勵(lì)。
在項(xiàng)目添加了獎(jiǎng)勵(lì)之后,我們進(jìn)行測試,首先在終端創(chuàng)建3個(gè)地址:
hanru:part8_Transaction2 ruby$ ./blockchain.sh hhanru:part8_Transaction2 ruby$ ./blockchain.sh createwallet
hanru:part8_Transaction2 ruby$ ./blockchain.sh createwallet
hanru:part8_Transaction2 ruby$ ./blockchain.sh printaddresses
運(yùn)行結(jié)果如下:
接下來我們創(chuàng)建創(chuàng)世區(qū)塊,并進(jìn)行一次轉(zhuǎn)賬:
hanru:part8_Transaction2 ruby$ ./blockchain.sh createblockchain -address 1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXthanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt
hanru:part8_Transaction2 ruby$ ./blockchain.sh send -from 1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt -to 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2 -amount 4
hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt
hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2
運(yùn)行結(jié)果如下:
說明:
比如創(chuàng)世區(qū)塊中1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt地址有10個(gè)Token,
轉(zhuǎn)賬給19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2地址4個(gè)Token,
那么1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt的余額是16(轉(zhuǎn)賬找零6個(gè),加上得到獎(jiǎng)勵(lì)10個(gè)),
19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2的余額是4。
4.2 UTXO 集
在持久化的章節(jié)中,我們研究了 Bitcoin Core 是如何在一個(gè)數(shù)據(jù)庫中存儲塊的,并且了解到區(qū)塊被存儲在?blocks?數(shù)據(jù)庫,交易輸出被存儲在?chainstate?數(shù)據(jù)庫。會回顧一下?chainstate?的結(jié)構(gòu):
c?+ 32 字節(jié)的交易哈希 -> 該筆交易的未花費(fèi)交易輸出記錄
B?+ 32 字節(jié)的塊哈希 -> 未花費(fèi)交易輸出的塊哈希
在之前那篇文章中,雖然我們已經(jīng)實(shí)現(xiàn)了交易,但是并沒有使用?chainstate?來存儲交易的輸出。所以,接下來我們繼續(xù)完成這部分。
chainstate?不存儲交易。它所存儲的是 UTXO 集,也就是未花費(fèi)交易輸出的集合。除此以外,它還存儲了“數(shù)據(jù)庫表示的未花費(fèi)交易輸出的塊哈希”,不過我們會暫時(shí)略過塊哈希這一點(diǎn),因?yàn)槲覀冞€沒有用到塊高度(但是我們會在接下來的文章中繼續(xù)改進(jìn))。
那么,我們?yōu)槭裁葱枰?UTXO 集呢?
來思考一下我們早先實(shí)現(xiàn)的在Blockchain類中的findUnspentTransactions()?方法:
/*** 查找錢包地址對應(yīng)的所有未花費(fèi)的交易
*
* @param pubKeyHash 錢包公鑰Hash
* @return
*/
private Transaction[] findUnspentTransactions(byte[] pubKeyHash) throws Exception {
Mapint[]> allSpentTXOs = this.getAllSpentTXOs(pubKeyHash);
Transaction[] unspentTxs = {};// 再次遍歷所有區(qū)塊中的交易輸出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;
}
這個(gè)函數(shù)找到有未花費(fèi)輸出的交易。由于交易被保存在區(qū)塊中,所以它會對區(qū)塊鏈里面的每一個(gè)區(qū)塊進(jìn)行迭代,檢查里面的每一筆交易。截止 2017 年 9 月 18 日,在比特幣中已經(jīng)有 485,860 個(gè)塊,整個(gè)數(shù)據(jù)庫所需磁盤空間超過 140 Gb。這意味著一個(gè)人如果想要驗(yàn)證交易,必須要運(yùn)行一個(gè)全節(jié)點(diǎn)。此外,驗(yàn)證交易將會需要在許多塊上進(jìn)行迭代。
整個(gè)問題的解決方案是有一個(gè)僅有未花費(fèi)輸出的索引,這就是 UTXO 集要做的事情:這是一個(gè)從所有區(qū)塊鏈交易中構(gòu)建(對區(qū)塊進(jìn)行迭代,但是只須做一次)而來的緩存,然后用它來計(jì)算余額和驗(yàn)證新的交易。截止 2017 年 9 月,UTXO 集大概才有 2.7 Gb。
好了,讓我們來想一下,為了實(shí)現(xiàn) UTXOs 池我們需要做哪些事情。當(dāng)前,有下列方法被用于查找交易信息:
Blockchain.getAllSpentTXOs?—— 查詢所有已被花費(fèi)的交易輸出。它需要遍歷區(qū)塊鏈中所有區(qū)塊中交易信息。
Blockchain.findUnspentTransactions?—— 查詢包含未被花費(fèi)的交易輸出的交易信息。它也需要遍歷區(qū)塊鏈中所有區(qū)塊中交易信息。
Blockchain.findSpendableOutputs?—— 該方法用于新的交易創(chuàng)建之時(shí)。它需要找到足夠多的交易輸出,以滿足所需支付的金額。需要調(diào)用?Blockchain.findUnspentTransactions?方法。
Blockchain.findUTXO?—— 查詢錢包地址所對應(yīng)的所有未花費(fèi)交易輸出,然后用于計(jì)算錢包余額。需要調(diào)用
Blockchain.findUnspentTransactions?方法。
Blockchain.findTransaction?—— 通過交易ID查詢交易信息。它需要遍歷所有的區(qū)塊直到找到交易信息為止。
如你所見,上面這些方法都需要去遍歷數(shù)據(jù)庫中的所有區(qū)塊。由于UTXOs池只存儲未被花費(fèi)的交易輸出,而不會存儲所有的交易信息,因此我們不會對有?Blockchain.findTransaction?進(jìn)行優(yōu)化。
那么,我們需要下列這些方法:
Blockchain.findUTXO?—— 通過遍歷所有的區(qū)塊來找到所有未被花費(fèi)的交易輸出.
UTXOSet.reindex?—— 調(diào)用上面?findUTXO?方法,然后將查詢結(jié)果存儲在數(shù)據(jù)庫中。也即需要進(jìn)行緩存的地方。
UTXOSet.findSpendableOutputs?—— 與?Blockchain.findSpendableOutputs?類似,區(qū)別在于會使用 UTXO 池。
UTXOSet.findUTXO?—— 與Blockchain.findUTXO?類似,區(qū)別在于會使用 UTXO 池。
Blockchain.findTransaction?—— 邏輯保持不變。
這樣,兩個(gè)使用最頻繁的方法將從現(xiàn)在開始使用緩存!
接下來,我們個(gè)通過代碼實(shí)現(xiàn)一下:
首先,我們在cldy.hanru.blockchain.transaction包下再創(chuàng)建一個(gè)java文件,命名為:UTXOSet.java。添加一個(gè)類:
/*** 未被花費(fèi)的交易輸出池
* @author hanru
*/
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class UTXOSet {
private Blockchain blockchain;
}
我們將會使用一個(gè)單一數(shù)據(jù)庫,但是我們會將 UTXO 集從存儲在不同的 bucket 中。因此,UTXOSet?跟?Blockchain?一起。
因?yàn)槲覀冃枰袀€(gè)bucket來存儲所有的UTXO,所以我們修改RocksDBUtils.java文件,添加對應(yīng)的存儲操作方法。
首先先定義存儲UTXO的bucket
/*** 鏈狀態(tài)桶Key
*/
private static final String CHAINSTATE_BUCKET_KEY = "chainstate";
/**
* chainstate buckets
*/
@Getter
private Map<String, byte[]> chainstateBucket;
接下來定義一些工具方法,initChainStateBucket()方法用于初始化bucket,并在該類的構(gòu)造函數(shù)中調(diào)用:
/*** 初始化 blocks 數(shù)據(jù)桶
*/
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個(gè)方法,用于表示添加、獲取、刪除UTXO:
/*** 保存UTXO數(shù)據(jù)
*
* @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數(shù)據(jù)
*
* @param key 交易ID
*/
public TXOutput[] getUTXOs(String key) {
byte[] utxosByte = chainstateBucket.get(key);
if (utxosByte != null) {
return (TXOutput[]) SerializeUtils.deserialize(utxosByte);
}
return null;
}
/**
* 刪除 UTXO 數(shù)據(jù)
*
* @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);
}
}
工具類準(zhǔn)備好后,我們在UTXOSet.java中,繼續(xù)定義一個(gè)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 ! ");
}
這個(gè)方法初始化了 UTXO 集。首先,先清除之前的bucket。然后從區(qū)塊鏈中獲取所有的未花費(fèi)輸出,最終將輸出保存到 bucket 中。該方法也用于重置UTXO集。
在Blockchain.java中添加方法,用于查詢所有的未花費(fèi)輸出:
/*** 查找所有的 unspent transaction outputs
*
* @return
*/
public Map findAllUTXOs() {
Mapint[]> allSpentTXOs = this.getAllSpentTXOs();
Map allUTXOs = new HashMap();// 再次遍歷所有區(qū)塊中的交易輸出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()方法代碼如下:
/*** 創(chuàng)建區(qū)塊鏈
*
* @param address
*/
private void createBlockchain(String address) {
Blockchain blockchain = Blockchain.createBlockchain(address);
UTXOSet utxoSet = new UTXOSet(blockchain);
utxoSet.reIndex();
log.info("Done ! ");
}
當(dāng)創(chuàng)建創(chuàng)世區(qū)塊后,就會立刻進(jìn)行初始化UTXO集。目前,即使這里看起來有點(diǎn)“殺雞用牛刀”,因?yàn)橐粭l鏈開始的時(shí)候,只有一個(gè)塊,里面只有一筆交易。
因?yàn)閯?chuàng)建了創(chuàng)世區(qū)塊,有CoinBase交易的10個(gè)Token,這是一個(gè)未花費(fèi)的TxOutput,找到之后可以存儲到UTXO集中。
4.3 優(yōu)化查詢余額
有了UTXO集,我們想要查詢余額,可以不用遍歷整個(gè)區(qū)塊鏈的所有區(qū)塊,而是直接查找UTXO集,找出對應(yīng)地址的utxo,進(jìn)行累加即可。
接下來,我們在UTXOSet.java中,添加findUTXOs(),用于查找指定賬戶的所有的utxo,代碼如下:
/*** 查找錢包地址對應(yīng)的所有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對象調(diào)用原來的查詢方法了,改用utxoSet對象進(jìn)行查詢余額,代碼如下:
/*** 查詢錢包余額
*
* @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);
}
接下來,我們測試一下,先進(jìn)行一次轉(zhuǎn)賬(轉(zhuǎn)賬我們還沒有使用UTXO集優(yōu)化),然后再查詢余額。在終端中輸入以下命令:
# 首先重新編譯程序hanru:part8_Transaction2 ruby$ mvn package
# 創(chuàng)建新的錢包地址
hanru:part8_Transaction2 ruby$ ./blockchain.sh createwallet
# 轉(zhuǎn)賬,19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2賬戶因?yàn)橹暗霓D(zhuǎn)賬操作,有4個(gè)Token
hanru:part8_Transaction2 ruby$ ./blockchain.sh send -from 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2 -to 17mW1XamRdFZBsnmfh3DxbC5pW1AttJznn -amount 3
# 查詢余額
hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2
hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 17mW1XamRdFZBsnmfh3DxbC5pW1AttJznn
運(yùn)行結(jié)果如下:
5. 總結(jié)
本章節(jié)中,我們并沒有在項(xiàng)目中新增功能,只是做了一些優(yōu)化。首先加入了獎(jiǎng)勵(lì)Reward機(jī)制。雖然程序中我們實(shí)現(xiàn)的比較建議,僅僅是給發(fā)起轉(zhuǎn)賬的人獎(jiǎng)勵(lì)10個(gè)Token,(如果一次轉(zhuǎn)賬多筆交易,只獎(jiǎng)勵(lì)給第一個(gè)轉(zhuǎn)賬人)。然后我們引入了UTXO集,進(jìn)行項(xiàng)目代碼優(yōu)化。UTXO集的原理,就是我們將所有區(qū)塊的未花費(fèi)的utxo,單獨(dú)存儲到一個(gè)bucket中。無論是進(jìn)行余額查詢,還是轉(zhuǎn)賬操作,都無需遍歷查詢所有的區(qū)塊,查詢所有的交易去找未花費(fèi)utxo了,只需要查詢UTXO集即可。如果是轉(zhuǎn)賬操作,轉(zhuǎn)賬后需要及時(shí)更新UTXO集。
總結(jié)
以上是生活随笔為你收集整理的修改linq结果集_UTXO集优化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: hosts文件_电脑修改hosts文件屏
- 下一篇: ieee39节点系统介绍_太原理工大学