在这个系列文章的一开始,我们就提到了,区块链是一个分布式数据库。不过在之前的文章中,我们选择性地跳过了“分布式”这个部分,而是将注意力都放到了“数据库”部分。到目前为止,我们几乎已经实现了一个区块链数据库的所有元素(基本原型,工作量证明,持久化,命令行接口,交易,地址,数字签名等)。今天,我们将会分析之前跳过的一些机制。而在下一篇文章中,我们将会开始讨论区块链的分布式特性。
本文的代码实现变化很大。
创建钱包地址,创建创世区块效果图:
使用UTXOSet优化后的转账,查询余额效果图:
###3.1 创建工程
打开IntelliJ IDEA的工作空间,将上一个项目代码目录part7_Signature
,复制为part8_Transaction2
。
然后打开IntelliJ IDEA开发工具。
打开工程:part8_Transaction2
,并删除target目录。然后进行以下修改:
step1:先将项目重新命名为:part8_Transaction2。
step2:修改pom.xml配置文件。
改为:<artifactId>part8_Transaction2</artifactId>标签
改为:<name>part8_Transaction2 Maven Webapp</name>
说明:我们每一章节的项目代码,都是在上一个章节上进行添加。所以拷贝上一次的项目代码,然后进行新内容的添加或修改。
###
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
*/
@Slf4j
public 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 Map<String, byte[]> 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);
}
}
//-------------------UTXOSet---------------------
/**
* chainstate buckets
*/
@Getter
private Map<String, byte[]> 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);
}
}
}
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
@Slf4j
public class UTXOSet {
private Blockchain blockchain;
/**
* 重建 UTXO 池索引
*/
@Synchronized
public void reIndex() {
log.info("Start to reIndex UTXO set !");
RocksDBUtils.getInstance().cleanChainStateBucket();
Map<String, TXOutput[]> allUTXOs = blockchain.findAllUTXOs();
for (Map.Entry<String, TXOutput[]> 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 = {};
Map<String, byte[]> 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) {
Map<String, int[]> unspentOuts = new HashMap<>();
int accumulated = 0;
Map<String, byte[]> chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket();
for (Map.Entry<String, byte[]> 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 最新的区块
*/
@Synchronized
public 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);
}
}
}
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
@AllArgsConstructor
public 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);
// Block genesisBlock = Block.newGenesisBlock();
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();
}
/**
* 根据data添加区块
* @param data
*/
// public void addBlock(String data) throws Exception{
//
// String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();
// if (StringUtils.isBlank(lastBlockHash)){
// throw new Exception("还没有数据库,无法直接添加区块。。");
// }
// this.addBlock(Block.newBlock(lastBlockHash,data));
// }
/**
* 区块链迭代器:内部类
*/
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<Transaction> 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;
}
/**
* 查找钱包地址对应的所有未花费的交易
*
* @param pubKeyHash 钱包公钥Hash
* @return
*/
/*
private Transaction[] findUnspentTransactions(byte[] pubKeyHash) throws Exception {
Map<String, int[]> 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].canBeUnlockedWith(address)) {
if (transaction.getOutputs()[outIndex].isLockedWithKey(pubKeyHash)) {
unspentTxs = ArrayUtils.add(unspentTxs, transaction);
}
}
}
}
return unspentTxs;
}
*/
/**
* 查找钱包地址对应的所有UTXO
*
* @param pubKeyHash 钱包公钥Hash
* @return
*/
/*
public TXOutput[] findUTXO(byte[] pubKeyHash) throws Exception {
Transaction[] unspentTxs = this.findUnspentTransactions(pubKeyHash);
TXOutput[] utxos = {};
if (unspentTxs == null || unspentTxs.length == 0) {
return utxos;
}
for (Transaction tx : unspentTxs) {
for (TXOutput txOutput : tx.getOutputs()) {
// if (txOutput.canBeUnlockedWith(address)) {
if (txOutput.isLockedWithKey(pubKeyHash)) {
utxos = ArrayUtils.add(utxos, txOutput);
}
}
}
return utxos;
}
*/
/**
* 寻找能够花费的交易
*
* @param pubKeyHash 钱包公钥Hash
* @param amount 花费金额
*/
/*
public SpendableOutputResult findSpendableOutputs(byte[] pubKeyHash, int amount) throws Exception {
Transaction[] unspentTXs = this.findUnspentTransactions(pubKeyHash);
int accumulated = 0;
Map<String, int[]> unspentOuts = new HashMap<>();
for (Transaction tx : unspentTXs) {
String txId = Hex.encodeHexString(tx.getTxId());
for (int outId = 0; outId < tx.getOutputs().length; outId++) {
TXOutput txOutput = tx.getOutputs()[outId];
// if (txOutput.canBeUnlockedWith(address) && accumulated < amount) {
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);
}
*/
//----------------------Sign-------------------------
/**
* 依据交易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<String, Transaction> 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<String, Transaction> 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 ! ");
}
}
//------------------------UTXOSet--------------------------
/**
* 从交易输入中查询区块链中所有已被花费了的交易输出
*
* @return 交易ID以及对应的交易输出下标地址
*/
private Map<String, int[]> getAllSpentTXOs() {
// 定义TxId ——> spentOutIndex[],存储交易ID与已被花费的交易输出数组索引值
Map<String, int[]> 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()) {
// if (txInput.canUnlockOutputWith(address)) {
// if (txInput.usesKey(pubKeyHash)) {
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<String, TXOutput[]> findAllUTXOs() {
Map<String, int[]> allSpentTXOs = this.getAllSpentTXOs();
Map<String, TXOutput[]> 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;
}
}
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
@NoArgsConstructor
public 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, data);
TXInput txInput = new TXInput(new byte[]{}, -1, null, data.getBytes());
// 创建交易输出
// TXOutput txOutput = new TXOutput(SUBSIDY, to);
TXOutput txOutput = TXOutput.newTXOutput(SUBSIDY, to);
// 创建交易
Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput}, System.currentTimeMillis());
// 设置交易ID
tx.setTxId();
// tx.setTxId(tx.hash());
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(from, amount);
// SpendableOutputResult result = blockchain.findSpendableOutputs(pubKeyHash, amount);
SpendableOutputResult result = new UTXOSet(blockchain).findSpendableOutputs(pubKeyHash, amount);
int accumulated = result.getAccumulated();
Map<String, int[]> unspentOuts = result.getUnspentOuts();
if (accumulated < amount) {
throw new Exception("ERROR: Not enough funds");
}
Iterator<Map.Entry<String, int[]>> iterator = unspentOuts.entrySet().iterator();
TXInput[] txInputs = {};
while (iterator.hasNext()) {
Map.Entry<String, int[]> 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, from));
txInputs = ArrayUtils.add(txInputs, new TXInput(txId, outIndex, null, pubKey));
}
}
TXOutput[] txOutput = {};
// txOutput = ArrayUtils.add(txOutput, new TXOutput(amount, to));
txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput(amount, to));
if (accumulated > amount) {
// txOutput = ArrayUtils.add(txOutput, new TXOutput((accumulated - amount), from));
txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput((accumulated - amount), from));
}
Transaction newTx = new Transaction(null, txInputs, txOutput, System.currentTimeMillis());
newTx.setTxId();
// newTx.setTxId(newTx.hash());
// 进行交易签名
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<String, Transaction> 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<String, Transaction> 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;
}
}
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);
}
/**
* 验证入参
*
* @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 = Blockchain.newBlockchain();
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("\tData: " + block.getData());
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\tScriptSiq:%s\n" , in.getScriptSig());
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\tScriptPubKey:%s\n" , out.getScriptPubKey());
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);
// TXOutput[] txOutputs = blockchain.findUTXO(address);
// TXOutput[] txOutputs = blockchain.findUTXO(pubKeyHash);
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);
blockchain.mineBlock(new Transaction[]{transaction});
RocksDBUtils.getInstance().closeDB();
System.out.println("Success!");
*/
Blockchain blockchain = Blockchain.createBlockchain(from);
// 新交易
Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain);
// 奖励
Transaction rewardTx = Transaction.newCoinbaseTX(from, "");
List<Transaction> 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<String> 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);
}
}
}
blockchain.sh
脚本文件最后修改blockchain.sh
脚本文件,修改后内容如下:
#!/bin/bash
set -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 "$@"
##
在上一篇文章中,我们略过的一个小细节是挖矿奖励。现在,我们已经可以来完善这个细节了。
挖矿奖励,实际上就是一笔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<Transaction> 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, data);
TXInput txInput = new TXInput(new byte[]{}, -1, null, data.getBytes());
// 创建交易输出
// TXOutput txOutput = new TXOutput(SUBSIDY, to);
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(from, amount);
SpendableOutputResult result = blockchain.findSpendableOutputs(pubKeyHash, amount);
int accumulated = result.getAccumulated();
Map<String, int[]> unspentOuts = result.getUnspentOuts();
if (accumulated < amount) {
throw new Exception("ERROR: Not enough funds");
}
Iterator<Map.Entry<String, int[]>> iterator = unspentOuts.entrySet().iterator();
TXInput[] txInputs = {};
while (iterator.hasNext()) {
Map.Entry<String, int[]> 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, from));
txInputs = ArrayUtils.add(txInputs, new TXInput(txId, outIndex, null, pubKey));
}
}
TXOutput[] txOutput = {};
// txOutput = ArrayUtils.add(txOutput, new TXOutput(amount, to));
txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput(amount, to));
if (accumulated > amount) {
// txOutput = ArrayUtils.add(txOutput, new TXOutput((accumulated - amount), from));
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 h
hanru:part8_Transaction2 ruby$ ./blockchain.sh createwallet
hanru:part8_Transaction2 ruby$ ./blockchain.sh createwallet
hanru:part8_Transaction2 ruby$ ./blockchain.sh printaddresses
运行结果如下:
接下来我们创建创世区块,并进行一次转账:
hanru:part8_Transaction2 ruby$ ./blockchain.sh createblockchain -address 1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt
hanru: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
运行结果如下:
说明:
- 比如创世区块中1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt地址有10个Token,
- 转账给19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2地址4个Token,
- 那么1FSwdBA55ne6a3QWLnzH3PtPtbDbNHBkXt的余额是16(转账找零6个,加上得到奖励10个),
- 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2的余额是4。
在持久化的章节中,我们研究了 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 {
Map<String, int[]> 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].canBeUnlockedWith(address)) {
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 进行优化。
那么,我们需要下列这些方法:
这样,两个使用最频繁的方法将从现在开始使用缓存!
接下来,我们个通过代码实现一下:
首先,我们在cldy.hanru.blockchain.transaction
包下再创建一个java文件,命名为:UTXOSet.java
。添加一个类:
/**
* 未被花费的交易输出池
* @author hanru
*/
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public 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
*/
@Getter
private 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 池索引
*/
@Synchronized
public void reIndex() {
log.info("Start to reIndex UTXO set !");
RocksDBUtils.getInstance().cleanChainStateBucket();
Map<String, TXOutput[]> allUTXOs = blockchain.findAllUTXOs();
for (Map.Entry<String, TXOutput[]> 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<String, TXOutput[]> findAllUTXOs() {
Map<String, int[]> allSpentTXOs = this.getAllSpentTXOs();
Map<String, TXOutput[]> 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集中。
有了UTXO集,我们想要查询余额,可以不用遍历整个区块链的所有区块,而是直接查找UTXO集,找出对应地址的utxo,进行累加即可。
接下来,我们在UTXOSet.java
中,添加findUTXOs()
,用于查找指定账户的所有的utxo,代码如下:
/**
* 查找钱包地址对应的所有UTXO
*
* @param pubKeyHash 钱包公钥Hash
* @return
*/
public TXOutput[] findUTXOs(byte[] pubKeyHash) {
TXOutput[] utxos = {};
Map<String, byte[]> 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);
// TXOutput[] txOutputs = blockchain.findUTXO(address);
// TXOutput[] txOutputs = blockchain.findUTXO(pubKeyHash);
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个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
运行结果如下:
因为我们已经将区块链中的utxo都存储到UTXO集中,所以UTXO 集可以用于转账发送币。
在UTXOSet中,继续修改,实现转账。添加findSpendableOutputs()
方法,代码如下:
/**
* 寻找能够花费的交易
*
* @param pubKeyHash 钱包公钥Hash
* @param amount 花费金额
*/
public SpendableOutputResult findSpendableOutputs(byte[] pubKeyHash, int amount) {
Map<String, int[]> unspentOuts = Maps.newHashMap();
int accumulated = 0;
Map<String, byte[]> chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket();
for (Map.Entry<String, byte[]> 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);
}
以上方法用于找出转账需要用的SpendableOutputResult。
接下来,修改Transactio中的newUTXOTransaction()
方法。不在通过blockchain查找转账所需要的SpendableOutputResult,而是修改为UTXOSe来查找,修改代码如下:
/**
* 从 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(from, amount);
// SpendableOutputResult result = blockchain.findSpendableOutputs(pubKeyHash, amount);
SpendableOutputResult result = new UTXOSet(blockchain).findSpendableOutputs(pubKeyHash, amount);
...
// 进行交易签名
blockchain.signTransaction(newTx, senderWallet.getPrivateKey());
return newTx;
}
通过转账产生交易,一定会花费之前的utxo,再产生新的utxo。有了 UTXO 集,也就意味着我们的数据(交易)现在已经被分开存储:实际交易被存储在区块链中,未花费输出被存储在 UTXO 集中。这样一来,我们就需要一个良好的同步机制,因为我们想要 UTXO 集时刻处于最新状态,并且存储最新交易的输出。但是我们不想每生成一个新块,就重新生成索引,因为这正是我们要极力避免的频繁区块链扫描。因此,我们需要一个机制来更新 UTXO 集。接下来,我们在UTXOSet.java
文件中,添加一个Update()
方法,代码如下:
/**
* 更新UTXO池
* 当一个新的区块产生时,需要去做两件事情:
* 1)从UTXO池中移除花费掉了的交易输出;
* 2)保存新的未花费交易输出;
*
* @param tipBlock 最新的区块
*/
@Synchronized
public 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);
}
}
虽然这个方法看起来有点复杂,但是它所要做的事情非常直观。当挖出一个新块时,应该更新 UTXO 集。更新意味着移除已花费输出,并从新挖出来的交易中加入未花费输出。如果一笔交易的输出被移除,并且不再包含任何输出,那么这笔交易也应该被移除。相当简单!
最后,在CLI.java
文件中,修改send()
方法,通过utxoSet进行转账并且转账后更新。
/**
* 转账
*
* @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);
blockchain.mineBlock(new Transaction[]{transaction});
RocksDBUtils.getInstance().closeDB();
System.out.println("Success!");
*/
Blockchain blockchain = Blockchain.createBlockchain(from);
// 新交易
Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain);
// 奖励
Transaction rewardTx = Transaction.newCoinbaseTX(from, "");
List<Transaction> transactions = new ArrayList<>();
transactions.add(transaction);
transactions.add(rewardTx);
Block newBlock = blockchain.mineBlock(transactions);
new UTXOSet(blockchain).update(newBlock);
log.info("Success!");
}
现在,让我们来进行最后的测试,在终端输入一下命令:
# 首先重新编译程序
hanru:part8_Transaction2 ruby$ mvn package
# 查询余额
hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2
hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 17mW1XamRdFZBsnmfh3DxbC5pW1AttJznn
# 转账,19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2账户因为之前的转账操作,有11个Token
hanru:part8_Transaction2 ruby$ ./blockchain.sh send -from 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2 -to 17mW1XamRdFZBsnmfh3DxbC5pW1AttJznn -amount 8
# 查询余额
hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 19a5Lfex3v9EgFUuNG7n5qR2DaXe5RzUT2
hanru:part8_Transaction2 ruby$ ./blockchain.sh getbalance -address 17mW1XamRdFZBsnmfh3DxbC5pW1AttJznn
执行结果如下:
本章节中,我们并没有在项目中新增功能,只是做了一些优化。首先加入了奖励Reward机制。虽然程序中我们实现的比较建议,仅仅是给发起转账的人奖励10个Token,(如果一次转账多笔交易,只奖励给第一个转账人)。然后我们引入了UTXO集,进行项目代码优化。UTXO集的原理,就是我们将所有区块的未花费的utxo,单独存储到一个bucket中。无论是进行余额查询,还是转账操作,都无需遍历查询所有的区块,查询所有的交易去找未花费utxo了,只需要查询UTXO集即可。如果是转账操作,转账后需要及时更新UTXO集。