摘要:交易這一環節是整個比特幣系統當中最為關鍵的一環,并且區塊鏈唯一的目的就是通過安全的可信的方式來存儲交易信息,防止它們創建之后被人惡意篡改。在比特幣中,交易輸出先于交易輸入出現。
最終內容請以原文為準: https://wangwei.one/posts/9cf...引言
上一篇 文章,我們實現了區塊數據的持久化,本篇開始交易環節的實現。交易這一環節是整個比特幣系統當中最為關鍵的一環,并且區塊鏈唯一的目的就是通過安全的、可信的方式來存儲交易信息,防止它們創建之后被人惡意篡改。今天我們開始實現交易這一環節,但由于這是一個很大的話題,所以我們分為兩部分:第一部分我們將實現區塊鏈交易的基本機制,到第二部分,我們再來研究它的細節。
比特幣交易如果你開發過Web應用程序,為了實現支付系統,你可能會在數據庫中創建一些數據庫表:賬戶 和 交易記錄。賬戶用于存儲用戶的個人信息以及賬戶余額等信息,交易記錄用于存儲資金從一個賬戶轉移到另一個賬戶的記錄。但是在比特幣中,支付系統是以一種完全不一樣的方式實現的,在這里:
沒有賬戶
沒有余額
沒有地址
沒有 Coins(幣)
沒有發送者和接受者
由于區塊鏈是一個公開的數據庫,我們不希望存儲有關錢包所有者的敏感信息。Coins 不會匯總到錢包中。交易不會將資金從一個地址轉移到另一個地址。沒有可保存帳戶余額的字段或屬性。只有交易信息。那比特幣的交易信息里面到底存儲的是什么呢?
交易組成一筆比特幣的交易由 交易輸入 和 交易輸出 組成,數據結構如下:
/** * 交易 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class Transaction { /** * 交易的Hash */ private byte[] txId; /** * 交易輸入 */ private TXInput[] inputs; /** * 交易輸出 */ private TXOutput[] outputs; }
一筆交易的 交易輸入 其實是指向上一筆交易的交易輸出 (這個后面詳細說明)。我們錢包里面的 Coin(幣)實際是存儲在這些 交易輸出 里面。下圖表示了區塊鏈交易系統里面各個交易相互引用的關系:
注意:
有些 交易輸出 并不是由 交易輸入 產生,而是憑空產生的(后面會詳細介紹)。
但,交易輸入 必須指向某個 交易輸出,它不能憑空產生。
在一筆交易里面,交易輸入 可能會來自多筆交易所產生的 交易輸出。
在整篇文章中,我們將使用諸如“錢”,“硬幣”,“花費”,“發送”,“賬戶”等詞語。但比特幣中沒有這樣的概念,在比特幣交易中,交易信息是由 鎖定腳本 鎖定一個數值,并且只能被所有者的 解鎖腳本 解鎖。(解鈴還須系鈴人)
交易輸出讓我們先從交易輸出開始,他的數據結構如下:
/** * 交易輸出 * * @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; }
前面提到過,一個交易輸入指向的是某一筆交易的交易輸出:
txId 存儲的是某筆交易的ID值
txOutputIndex 存儲的是交易中這個交易輸出的索引位置(因為一筆交易可能包含多個交易輸出)
scriptSig 主要是提供用于交易輸出中 ScriptPubKey 所需的驗證數據。
如果這個數據被驗證正確,那么相應的交易輸出將被解鎖,并且其中的 value 能夠生成新的交易輸出;
如果不正確,那么相應的交易輸出將不能被交易輸入所引用;
通過鎖定腳本與解鎖腳本這種機制,保證了某個用戶不能花費屬于他人的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 也都需要做改變:
/** *創建創世區塊
* * @param coinbase * @return */ public static Block newGenesisBlock(Transaction coinbase) { return Block.newBlock("", new Transaction[]{coinbase}); } /** *創建新區塊
* * @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 方法:
/** *創建區塊鏈
* * @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)Pow算法必須將存儲在區塊中的交易信息考慮在內,以保存交易信息存儲的一致性和可靠性。因此,我們必須修改 ProofOfWork.prepareData 接口代碼邏輯:
/** * 準備數據 ** 注意:在準備區塊數據時,一定要從原始數據類型轉化為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(未花費交易輸出)
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如下:
tx0, output 1;
tx1, output 0;
tx3, output 0;
tx4, output 0.
當然,當我們檢查余額時,我不需要區塊鏈中所有的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 { MapallSpentTXOs = 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 getAllSpentTXOs(String address) throws Exception { // 定義TxId ——> spentOutIndex[],存儲交易ID與已被花費的交易輸出數組索引值 Map 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 ", 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(); MapunspentOuts = result.getUnspentOuts(); if (accumulated < amount) { throw new Exception("ERROR: Not enough funds"); } Iterator > iterator = unspentOuts.entrySet().iterator(); TXInput[] txInputs = {}; while (iterator.hasNext()) { Map.Entry 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 要干的事情。之后,為每個找到的輸出創建一個引用它的輸入。接下來,我們創建兩個交易輸出:
一個 output 用于鎖定到接收者的錢包地址上。這個是真正被轉走的coins;
另一個 output 鎖定到發送者的錢包地址上。這個就是 找零。只有當用于支付的UTXO總和大于要支付的金額時,才會創建這部分的 交易輸出。記住:交易輸出是不可分割的
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; MapunspentOuts = 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)總結
本篇內容有點難度,但好歹我們現在有了交易信息了。盡管,缺少像比特幣這一類加密貨幣的一些關鍵特性:
錢包地址。我們還沒有基于私鑰的真實地址。
獎勵。挖礦絕對沒有利潤。
UTXO集。當我們計算錢包地址的余額時,我們需要遍歷所有的區塊中的所有交易信息,當有許許多多的區塊時,這將花費不少的時間。此外,如果我們想驗證以后的交易,可能需要很長時間。 UTXO集旨在解決這些問題并快速處理交易。
內存池。 這是交易在打包成區塊之前存儲的地方。 在我們當前的實現中,一個塊只包含一筆交易,而且效率很低。
資料源代碼:https://github.com/wangweiX/b...
《精通比特幣(第二版)》第6章 —— 交易
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/24017.html
摘要:交易這一環節是整個比特幣系統當中最為關鍵的一環,并且區塊鏈唯一的目的就是通過安全的可信的方式來存儲交易信息,防止它們創建之后被人惡意篡改。在比特幣中,交易輸出先于交易輸入出現。 showImg(https://segmentfault.com/img/remote/1460000013923562?w=3200&h=1400); 最終內容請以原文為準: https://wangwei....
摘要:截止年月號,比特幣中有個區塊,并且這些數據占據了的磁盤空間。每個比特幣節點都是路由區塊鏈數據庫挖礦錢包服務的功能集合。是比特幣的輕量級節點,它不需要下載所有的區塊鏈數據,也不需要驗證區塊和交易數據。 showImg(https://img.i7years.com/blog/pexels-photo-38136.jpeg); 最終內容請以原文為準:https://wangwei.one/...
摘要:截止年月號,比特幣中有個區塊,并且這些數據占據了的磁盤空間。每個比特幣節點都是路由區塊鏈數據庫挖礦錢包服務的功能集合。是比特幣的輕量級節點,它不需要下載所有的區塊鏈數據,也不需要驗證區塊和交易數據。 showImg(https://img.i7years.com/blog/pexels-photo-38136.jpeg); 最終內容請以原文為準:https://wangwei.one/...
摘要:我們目前正處于一個新興的區塊鏈開發行業中。,一種在以太坊開發人員中流行的新的簡單編程語言,因為它是用于開發以太坊智能合約的語言。它是全球至少萬開發人員使用的世界上最流行的編程語言之一。以太坊,主要是針對工程師使用進行區塊鏈以太坊開發的詳解。 我們目前正處于一個新興的區塊鏈開發行業中。區塊鏈技術處于初期階段,然而這種顛覆性技術已經成功地風靡全球,并且最近經歷了一場與眾不同的繁榮。由于許多...
摘要:我們該選擇哪一款數據庫呢事實上,在比特幣白皮書中并沒有明確指定使用哪一種的數據庫,因此這個由開發人員自己決定。詳見精通比特幣第二版第章節交易的輸入與輸出此外,每個區塊數據都是以單獨的文件形式存儲在磁盤上。資料源代碼精通比特幣第二版 showImg(https://segmentfault.com/img/remote/1460000013923488?w=1200&h=627); 最...
閱讀 2222·2023-04-26 01:57
閱讀 3240·2023-04-25 16:30
閱讀 2325·2021-11-17 09:38
閱讀 1068·2021-10-08 10:14
閱讀 1383·2021-09-23 11:21
閱讀 3678·2019-08-29 17:28
閱讀 3450·2019-08-29 15:27
閱讀 944·2019-08-29 13:04