最终内容请以原文为准: https://wangwei.one/posts/9cf...
上一篇 文章,咱们实现了区块数据的持久化,本篇开始交易环节的实现。交易这一环节是整个比特币系统当中最为关键的一环,而且区块链惟一的目的就是经过安全的、可信的方式来存储交易信息,防止它们建立以后被人恶意篡改。今天咱们开始实现交易这一环节,但因为这是一个很大的话题,因此咱们分为两部分:第一部分咱们将实现区块链交易的基本机制,到第二部分,咱们再来研究它的细节。html
若是你开发过Web应用程序,为了实现支付系统,你可能会在数据库中建立一些数据库表:帐户
和 交易记录
。帐户用于存储用户的我的信息以及帐户余额等信息,交易记录用于存储资金从一个帐户转移到另外一个帐户的记录。可是在比特币中,支付系统是以一种彻底不同的方式实现的,在这里:前端
因为区块链是一个公开的数据库,咱们不但愿存储有关钱包全部者的敏感信息。Coins
不会汇总到钱包中。交易不会将资金从一个地址转移到另外一个地址。没有可保存账户余额的字段或属性。只有交易信息。那比特币的交易信息里面到底存储的是什么呢?java
一笔比特币的交易由 交易输入
和 交易输出
组成,数据结构以下:git
/** * 交易 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class Transaction { /** * 交易的Hash */ private byte[] txId; /** * 交易输入 */ private TXInput[] inputs; /** * 交易输出 */ private TXOutput[] outputs; }
一笔交易的 交易输入
实际上是指向上一笔交易的交易输出
(这个后面详细说明)。咱们钱包里面的 Coin(币)实际是存储在这些 交易输出
里面。下图表示了区块链交易系统里面各个交易相互引用的关系:github
注意:算法
交易输出
并非由 交易输入
产生,而是凭空产生的(后面会详细介绍)。交易输入
必须指向某个 交易输出
,它不能凭空产生。交易输入
可能会来自多笔交易所产生的 交易输出
。在整篇文章中,咱们将使用诸如“钱”,“硬币”,“花费”,“发送”,“帐户”等词语。但比特币中没有这样的概念,在比特币交易中,交易信息是由 锁定脚本
锁定一个数值,而且只能被全部者的 解锁脚本
解锁。(解铃还须系铃人)shell
让咱们先从交易输出开始,他的数据结构以下:数据库
/** * 交易输出 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class TXOutput { /** * 数值 */ private int value; /** * 锁定脚本 */ private String scriptPubKey; }
实际上,它表示的是可以存储 "coins(币)"的交易输出(注意 value 字段)。而且这里所谓的 value 其实是由存储在 ScriptPubKey (锁定脚本)中的一个puzzle(难题) 所锁定。在内部,比特币使用称为脚本的脚本语言,用于定义输出锁定和解锁逻辑。这个语言很原始(这是故意的,以免可能的黑客和滥用),但咱们不会详细讨论它。 你能够在这里找到它的详细解释。here数组
在比特币中,value 字段存储着 satoshis 的任意倍的数值,而不是BTC的数量。satoshis 是比特币的百万分之一(0.00000001 BTC),所以这是比特币中最小的货币单位(如1美分)。安全
satoshis:聪锁定脚本是一个放在一个输出值上的“障碍”,同时它明确了从此花费这笔输出的条件。因为锁定脚本每每含有一个公钥(即比特币地址),在历史上它曾被称做一个脚本公钥代码。在大多数比特币应用源代码中,脚本公钥代码即是咱们所说的锁定脚本。
因为咱们尚未实现钱包地址的逻辑,因此这里先暂且忽略锁定脚本相关的逻辑。ScriptPubKey 将会存储任意的字符串(用户定义的钱包地址)
顺便说一句,拥有这样的脚本语言意味着比特币也能够用做智能合约平台。
关于 交易输出
的一个重要的事情是它们是不可分割的,这意味着你不能将它所存储的数值拆开来使用。当这个交易输出在新的交易中被交易输入所引用时,它将做为一个总体被花费掉。 若是其值大于所需值,那么剩余的部分则会做为零钱返回给付款方。 这与真实世界的状况相似,例如,您支付5美圆的钞票用于购买1美圆的东西,那么你将会获得4美圆的零钱。
/** * 交易输入 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class TXInput { /** * 交易Id的hash值 */ private byte[] txId; /** * 交易输出索引 */ private int txOutputIndex; /** * 解锁脚本 */ private String scriptSig; }
前面提到过,一个交易输入指向的是某一笔交易的交易输出:
scriptSig 主要是提供用于交易输出中 ScriptPubKey 所需的验证数据。
经过锁定脚本与解锁脚本这种机制,保证了某个用户不能花费属于他人的Coins。
一样,因为咱们还没有实现钱包地址功能,ScriptSig 将会存储任意的用户所定义的钱包地址。咱们将会在下一章节实现公钥和数字签名验证。
说了这么多,咱们来总结一下。交易输出是"Coins"实际存储的地方。每个交易输出都带有一个锁定脚本,它决定了解锁的逻辑。每一笔新的交易必须至少有一个交易输入与交易输出。一笔交易的交易输入指向前一笔交易的交易输出,而且提供用于锁定脚本解锁须要的数据(ScriptSig
字段),而后利用交易输出中的 value
去建立新的交易输出。
注意,这段话的原文以下,可是里面有表述错误的地方,交易输出带有的是锁定脚本,而不是解锁脚本。Let’s sum it up. Outputs are where “coins” are stored. Each output comes with an unlocking script, which determines the logic of unlocking the output. Every new transaction must have at least one input and output. An input references an output from a previous transaction and provides data (the
ScriptSig
field) that is used in the output’s unlocking script to unlock it and use its value to create new outputs.
那究竟是先有交易输入仍是先有交易输出呢?
在比特币中,鸡蛋先于鸡出现。交易输入源自于交易输出的逻辑是典型的"先有鸡仍是先有蛋"的问题:交易输入产生交易输出,交易输出又会被交易输入所引用。在比特币中,交易输出先于交易输入出现。
当矿工开始开采区块时,区块中会被添加一个 coinbase 交易。coinbase 交易是一种特殊的交易,它不须要之前已经存在的交易输出。它会凭空建立出交易输出(i.e: Coins)。也即,鸡蛋的出现并不须要母鸡,这笔交易是做为矿工成功挖出新的区块后的一笔奖励。
正如你所知道的那样,在区块链的最前端,即第一个区块,有一个创世区块。他产生了区块链中有史以来的第一个交易输出,而且因为没有前一笔交易,也就没有相应的输出,所以不须要前一笔交易的交易输出。
让咱们来建立 coinbase 交易:
/** * 建立CoinBase交易 * * @param to 收帐的钱包地址 * @param data 解锁脚本数据 * @return */ public 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); // 建立交易输出 TXOutput txOutput = new TXOutput(SUBSIDY, to); // 建立交易 Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput}); // 设置交易ID tx.setTxId(); return tx; }
coinbase交易只有一个交易输入。在咱们的代码实现中,txId 是空数组,txOutputIndex 设置为了 -1。另外,coinbase交易不会在 ScriptSig 字段上存储解锁脚本,相反,存了一个任意的数据。
在比特币中,第一个 coinbase 交易报刊了以下的信息:"The Times 03/Jan/2009 Chancellor on brink of second bailout for banks". 点击查看
SUBSIDY 是挖矿奖励数量。在比特币中,这个奖励数量没有存储在任何地方,而是依据现有区块的总数进行计算而获得:区块总数 除以 210000。开采创世区块获得的奖励为50BTC,每过 210000 个区块,奖励会减半。在咱们的实现中,咱们暂且将挖矿奖励设置为常数。(至少目前是这样)
从如今开始,每个区块必须存储至少一个交易信息,而且尽量地避免在没有交易数据的状况下进行挖矿。这意味着咱们必须移除 Block 对象中的 date 字段,取而代之的是 transactions:
/** * 区块 * * @author wangwei * @date 2018/02/02 */ @Data @AllArgsConstructor @NoArgsConstructor public class Block { /** * 区块hash值 */ private String hash; /** * 前一个区块的hash值 */ private String previousHash; /** * 交易信息 */ private Transaction[] transactions; /** * 区块建立时间(单位:秒) */ private long timeStamp; }
相应地,newGenesisBlock 与 newBlock 也都须要作改变:
/** * <p> 建立创世区块 </p> * * @param coinbase * @return */ public static Block newGenesisBlock(Transaction coinbase) { return Block.newBlock("", new Transaction[]{coinbase}); } /** * <p> 建立新区块 </p> * * @param previousHash * @param transactions * @return */ public static Block newBlock(String previousHash, Transaction[] transactions) { Block block = new Block("", previousHash, transactions, Instant.now().getEpochSecond(), 0); ProofOfWork pow = ProofOfWork.newProofOfWork(block); PowResult powResult = pow.run(); block.setHash(powResult.getHash()); block.setNonce(powResult.getNonce()); return block; }
接下来,修改 newBlockchain 方法:
/** * <p> 建立区块链 </p> * * @param address 钱包地址 * @return */ public static Blockchain newBlockchain(String address) throws Exception { String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); if (StringUtils.isBlank(lastBlockHash)) { // 建立 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); }
如今,代码有钱包地址的接口,将会收到开采创世区块的奖励。
Pow算法必须将存储在区块中的交易信息考虑在内,以保存交易信息存储的一致性和可靠性。所以,咱们必须修改 ProofOfWork.prepareData 接口代码逻辑:
/** * 准备数据 * <p> * 注意:在准备区块数据时,必定要从原始数据类型转化为byte[],不能直接从字符串进行转换 * @param nonce * @return */ private String prepareData(long nonce) { byte[] prevBlockHashBytes = {}; if (StringUtils.isNoneBlank(this.getBlock().getPrevBlockHash())) { prevBlockHashBytes = new BigInteger(this.getBlock().getPrevBlockHash(), 16).toByteArray(); } return ByteUtils.merge( prevBlockHashBytes, this.getBlock().hashTransaction(), ByteUtils.toBytes(this.getBlock().getTimeStamp()), ByteUtils.toBytes(TARGET_BITS), ByteUtils.toBytes(nonce) ); }
其中 hashTransaction 代码以下:
/** * 对区块中的交易信息进行Hash计算 * * @return */ public byte[] hashTransaction() { byte[][] txIdArrays = new byte[this.getTransactions().length][]; for (int i = 0; i < this.getTransactions().length; i++) { txIdArrays[i] = this.getTransactions()[i].getTxId(); } return DigestUtils.sha256(ByteUtils.merge(txIds)); }
一样,咱们使用哈希值来做为数据的惟一标识。咱们但愿区块中的全部交易数据都能经过一个哈希值来定义它的惟一标识。为了达到这个目的,咱们计算了每个交易的惟一哈希值,而后将他们串联起来,再对这个串联后的组合进行哈希值计算。
比特币使用更复杂的技术:它将全部包含在块中的交易表示为 Merkle树 ,并在Proof-of-Work系统中使用该树的根散列。 这种方法只须要跟节点的散列值就能够快速检查块是否包含某笔交易,而无需下载全部交易。
UTXO:unspend transaction output.(未被花费的交易输出)在比特币的世界里既没有帐户,也没有余额,只有分散到区块链里的UTXO.
UTXO 是理解比特币交易原理的关键所在,咱们先来看一段场景:
场景:假设你过去分别向A、B、C这三个比特币用户购买了BTC,从A手中购买了3.5个BTC,从B手中购买了4.5个BTC,从C手中购买了2个BTC,如今你的比特币钱包里面刚好剩余10个BTC。
问题:这个10个BTC是真正的10个BTC吗?其实不是,这句话可能听起来有点怪。(什么!我钱包里面的BTC不是真正的BTC,你不要吓我……)
解释:前面提到过在比特币的交易系统当中,并不存在帐户、余额这些概念,因此,你的钱包里面的10个BTC,并非说钱包余额为10个BTC。而是说,这10个BTC实际上是由你的比特币地址(钱包地址|公钥)锁定了的散落在各个区块和各个交易里面的UTXO的总和。
UTXO 是比特币交易的基本单位,每笔交易都会产生UTXO,一个UTXO能够是一“聪”的任意倍。给某人发送比特币其实是创造新的UTXO,绑定到那我的的钱包地址,而且能被他用于新的支付。
通常的比特币交易由 交易输入
和 交易输出
两部分组成。A向你支付3.5个BTC这笔交易,实际上产生了一个新的UTXO,这个新的UTXO 等于 3.5个BTC(3.5亿聪),而且锁定到了你的比特币钱包地址上。
假如你要给你女(男)朋友转 1.5 BTC,那么你的钱包会从可用的UTXO中选取一个或多个可用的个体来拼凑出一个大于或等于一笔交易所需的比特币量。好比在这个假设场景里面,你的钱包会选取你和C的交易中的UTXO做为 交易输入,input = 2BTC,这里会生成两个新的交易输出,一个输出(UTXO = 1.5 BTC)会被绑定到你女(男)朋友的钱包地址上,另外一个输出(UTXO = 0.5 BTC)会做为找零,从新绑定到你的钱包地址上。
有关比特币交易这部分更详细的内容,请查看: 《精通比特币(第二版)》第6章 —— 交易
咱们须要找到全部未花费的交易输出(UTXO)。Unspent(未花费) 意味着这些交易输出从未被交易输入所指向。这前面的图片中,UTXO以下:
固然,当咱们检查余额时,我不须要区块链中全部的UTXO,我只须要能被咱们解锁的UTXO(当前,咱们尚未实现密钥对,而是替代为用户自定义的钱包地址)。首先,咱们在交易输入与交易输出上定义锁定-解锁的方法:
交易输入:
public class TXInput { ... /** * 判断解锁数据是否可以解锁交易输出 * * @param unlockingData * @return */ public boolean canUnlockOutputWith(String unlockingData) { return this.getScriptSig().endsWith(unlockingData); } }
交易输出:
public class TXOutput { ... /** * 判断解锁数据是否可以解锁交易输出 * * @param unlockingData * @return */ public boolean canBeUnlockedWith(String unlockingData) { return this.getScriptPubKey().endsWith(unlockingData); } }
这里咱们暂时用 unlockingData 来与脚本字段进行比较。咱们会在后面的文章中来对这部份内容进行优化,咱们将会基于私钥来实现用户的钱包地址。
下一步,查询全部与钱包地址绑定的包含UTXO的交易信息,有点复杂(本篇先这样实现,后面咱们作一个与钱包地址映射的UTXO池来进行优化):
public class Blockchain { ... /** * 查找钱包地址对应的全部未花费的交易 * * @param address 钱包地址 * @return */ private Transaction[] findUnspentTransactions(String address) throws Exception { Map<String, int[]> allSpentTXOs = this.getAllSpentTXOs(address); 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)) { unspentTxs = ArrayUtils.add(unspentTxs, transaction); } } } } return unspentTxs; } /** * 从交易输入中查询区块链中全部已被花费了的交易输出 * * @param address 钱包地址 * @return 交易ID以及对应的交易输出下标地址 * @throws Exception */ private Map<String, int[]> getAllSpentTXOs(String address) throws Exception { // 定义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)) { 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; } ... }
获得了全部包含UTXO的交易数据,接下来,咱们就能够获得全部UTXO集合了:
public class Blockchain { ... /** * 查找钱包地址对应的全部UTXO * * @param address 钱包地址 * @return */ public TXOutput[] findUTXO(String address) throws Exception { Transaction[] unspentTxs = this.findUnspentTransactions(address); TXOutput[] utxos = {}; if (unspentTxs == null || unspentTxs.length == 0) { return utxos; } for (Transaction tx : unspentTxs) { for (TXOutput txOutput : tx.getOutputs()) { if (txOutput.canBeUnlockedWith(address)) { utxos = ArrayUtils.add(utxos, txOutput); } } } return utxos; } ... }
如今,咱们能够实现获取钱包地址余额的接口了:
public class CLI { ... /** * 查询钱包余额 * * @param address 钱包地址 */ private void getBalance(String address) throws Exception { Blockchain blockchain = Blockchain.createBlockchain(address); TXOutput[] txOutputs = blockchain.findUTXO(address); 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); } ... }
查询 wangwei 这个钱包地址的余额:
$ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei # 输出 Balance of 'wangwei': 10
如今,咱们想要给某人发送一些币。所以,咱们须要建立一笔新的交易,而后放入区块中,再进行挖矿。到目前为止,咱们只是实现了 coinbase 交易,如今咱们须要实现常见的建立交易接口:
public class Transaction { ... /** * 从 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 { SpendableOutputResult result = blockchain.findSpendableOutputs(from, 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)); } } TXOutput[] txOutput = {}; txOutput = ArrayUtils.add(txOutput, new TXOutput(amount, to)); if (accumulated > amount) { txOutput = ArrayUtils.add(txOutput, new TXOutput((accumulated - amount), from)); } Transaction newTx = new Transaction(null, txInputs, txOutput); newTx.setTxId(); return newTx; } ... }
在建立新的交易输出以前,咱们须要事先找到全部的UTXO,并确保有足够的金额。这就是 findSpendableOutputs 要干的事情。以后,为每一个找到的输出建立一个引用它的输入。接下来,咱们建立两个交易输出:
findSpendableOutputs 须要调用咱们以前建立的 findUnspentTransactions 接口:
public class Blockchain { ... /** * 寻找可以花费的交易 * * @param address 钱包地址 * @param amount 花费金额 */ public SpendableOutputResult findSpendableOutputs(String address, int amount) throws Exception { Transaction[] unspentTXs = this.findUnspentTransactions(address); 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) { 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并统计他们的总额。当计算的总额刚好大于或者等于须要转帐的金额时,方法会中止遍历,而后返回用于支付的总额以及按交易ID分组的交易输出索引值数组。咱们不想要花更多的钱。
如今,咱们能够修改 Block.mineBlock 接口:
public class Block { ... /** * 打包交易,进行挖矿 * * @param transactions */ public void mineBlock(Transaction[] transactions) throws Exception { String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); if (lastBlockHash == null) { throw new Exception("ERROR: Fail to get last block hash ! "); } Block block = Block.newBlock(lastBlockHash, transactions); this.addBlock(block); } ... }
最后,咱们来实现转帐的接口:
public class CLI { ... /** * 转帐 * * @param from * @param to * @param amount */ private void send(String from, String to, int amount) throws Exception { 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!"); } ... }
转帐,意味着建立一笔新的交易而且经过挖矿的方式将其存入区块中。可是,比特币不会像咱们这样作,它会把新的交易记录先存到内存池中,当一个矿工准备去开采一个区块时,它会把打包内存池中的全部交易信息,而且建立一个候选区块。只有当这个包含全部交易信息的候选区块被成功开采而且被添加到区块链上时,这些交易信息才算被确认。
让咱们来测试一下:
# 先确认 wangwei 的余额 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei Balance of 'wangwei': 10 # 转帐 $ java -jar blockchain-java-jar-with-dependencies.jar send -from wangwei -to Pedro -amount 6 Elapsed Time: 0.828 seconds correct hash Hex: 00000c5f50cf72db1f375a5d454f98bc49d07335db921cbef5fa9e58ad34d462 Success! # 查询 wangwei 的余额 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei Balance of 'wangwei': 4 # 查询 Pedro 的余额 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address Pedro Balance of 'Pedro': 6
赞!如今让咱们来建立更多的交易而且确保从多个交易输出进行转帐是正常的:
$ java -jar blockchain-java-jar-with-dependencies.jar send -from Pedro -to Helen -amount 2 Elapsed Time: 2.533 seconds correct hash Hex: 00000c81d541ad407a3767ad633d1147602df86fe14e1962ec145ab17b633e88 Success! $ java -jar blockchain-java-jar-with-dependencies.jar send -from wangwei -to Helen -amount 2 Elapsed Time: 1.481 seconds correct hash Hex: 00000c3f8b82c2b970438f5f1f39d56bb8a9d66341efc92a02ffcbff91acd84b Success!
如今,Helen 这个钱包地址上有了两笔从 wangwei 和 Pedro 转帐中产生的UTXO,让咱们将它们再转帐给另一我的:
$ java -jar blockchain-java-jar-with-dependencies.jar send -from Helen -to Rachel -amount 3 Elapsed Time: 17.136 seconds correct hash Hex: 000000b1226a947166c2b01a15d1cd3558ddf86fe99bad28a0501a2af60f6a02 Success! $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei Balance of 'wangwei': 2 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address Pedro Balance of 'Pedro': 4 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address Helen Balance of 'Helen': 1 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address Rachel Balance of 'Rachel': 3
很是棒!让咱们来测试一下失败的场景:
$ java -jar blockchain-java-jar-with-dependencies.jar send -from wangwei -to Ivan -amount 5 java.lang.Exception: ERROR: Not enough funds at one.wangwei.blockchain.transaction.Transaction.newUTXOTransaction(Transaction.java:104) at one.wangwei.blockchain.cli.CLI.send(CLI.java:138) at one.wangwei.blockchain.cli.CLI.parse(CLI.java:73) at one.wangwei.blockchain.cli.Main.main(Main.java:7)
本篇内容有点难度,但好歹咱们如今有了交易信息了。尽管,缺乏像比特币这一类加密货币的一些关键特性: