最終內容請以原文爲準: 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)
本篇內容有點難度,但好歹咱們如今有了交易信息了。儘管,缺乏像比特幣這一類加密貨幣的一些關鍵特性: