基于Java语言构建区块链(四)—— 交易(UTXO)
基于Java語(yǔ)言構(gòu)建區(qū)塊鏈(四)—— 交易(UTXO)
2018年03月11日 00:48:01?wangwei_hz?閱讀數(shù):909?標(biāo)簽:?區(qū)塊鏈比特幣?更多
個(gè)人分類:?區(qū)塊鏈
文章的主要思想和內(nèi)容均來(lái)自?https://jeiwan.cc/posts/building-blockchain-in-go-part-4/
原文鏈接:https://wangwei.one/posts/9cf9e42a.html
引言
交易這一環(huán)節(jié)是整個(gè)比特幣系統(tǒng)當(dāng)中最為關(guān)鍵的一環(huán),并且區(qū)塊鏈唯一的目的就是通過(guò)安全的、可信的方式來(lái)存儲(chǔ)交易信息,防止它們創(chuàng)建之后被人惡意篡改。今天我們開(kāi)始實(shí)現(xiàn)交易這一環(huán)節(jié),但由于這是一個(gè)很大的話題,所以我們分為兩部分:第一部分我們將實(shí)現(xiàn)區(qū)塊鏈交易的基本機(jī)制,到第二部分,我們?cè)賮?lái)研究它的細(xì)節(jié)。
比特幣交易
如果你開(kāi)發(fā)過(guò)Web應(yīng)用程序,為了實(shí)現(xiàn)支付系統(tǒng),你可能會(huì)在數(shù)據(jù)庫(kù)中創(chuàng)建一些數(shù)據(jù)庫(kù)表:賬戶?和?交易記錄。賬戶用于存儲(chǔ)用戶的個(gè)人信息以及賬戶余額等信息,交易記錄用于存儲(chǔ)資金從一個(gè)賬戶轉(zhuǎn)移到另一個(gè)賬戶的記錄。但是在比特幣中,支付系統(tǒng)是以一種完全不一樣的方式實(shí)現(xiàn)的,在這里:
- 沒(méi)有賬戶
- 沒(méi)有余額
- 沒(méi)有地址
- 沒(méi)有 Coins(幣)
- 沒(méi)有發(fā)送者和接受者
由于區(qū)塊鏈?zhǔn)且粋€(gè)公開(kāi)的數(shù)據(jù)庫(kù),我們不希望存儲(chǔ)有關(guān)錢包所有者的敏感信息。Coins?不會(huì)匯總到錢包中。交易不會(huì)將資金從一個(gè)地址轉(zhuǎn)移到另一個(gè)地址。沒(méi)有可保存帳戶余額的字段或?qū)傩?。只有交易信息。那比特幣的交易信息里面到底存?chǔ)的是什么呢?
交易組成
一筆比特幣的交易由?交易輸入?和?交易輸出?組成,數(shù)據(jù)結(jié)構(gòu)如下:
/*** 交易** @author wangwei* @date 2017/03/04*/ @Data @AllArgsConstructor @NoArgsConstructor public class Transaction {/*** 交易的Hash*/private byte[] txId;/*** 交易輸入*/private TXInput[] inputs;/*** 交易輸出*/private TXOutput[] outputs;}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
一筆交易的?交易輸入?其實(shí)是指向上一筆交易的交易輸出?(這個(gè)后面詳細(xì)說(shuō)明)。我們錢包里面的 Coin(幣)實(shí)際是存儲(chǔ)在這些?交易輸出?里面。下圖表示了區(qū)塊鏈交易系統(tǒng)里面各個(gè)交易相互引用的關(guān)系:
注意:
在整篇文章中,我們將使用諸如“錢”,“硬幣”,“花費(fèi)”,“發(fā)送”,“賬戶”等詞語(yǔ)。但比特幣中沒(méi)有這樣的概念,在比特幣交易中,交易信息是由?鎖定腳本?鎖定一個(gè)數(shù)值,并且只能被所有者的?解鎖腳本?解鎖。(解鈴還須系鈴人)
交易輸出
讓我們先從交易輸出開(kāi)始,他的數(shù)據(jù)結(jié)構(gòu)如下:
/*** 交易輸出** @author wangwei* @date 2017/03/04*/ @Data @AllArgsConstructor @NoArgsConstructor public class TXOutput {/*** 數(shù)值*/private int value;/*** 鎖定腳本*/private String scriptPubKey;}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
實(shí)際上,它表示的是能夠存儲(chǔ) “coins(幣)”的交易輸出(注意?value?字段)。并且這里所謂的?value?實(shí)際上是由存儲(chǔ)在ScriptPubKey?(鎖定腳本)中的一個(gè)puzzle(難題) 所鎖定。在內(nèi)部,比特幣使用稱為腳本的腳本語(yǔ)言,用于定義輸出鎖定和解鎖邏輯。這個(gè)語(yǔ)言很原始(這是故意的,以避免可能的黑客和濫用),但我們不會(huì)詳細(xì)討論它。 你可以在這里找到它的詳細(xì)解釋。here
在比特幣中,value?字段存儲(chǔ)著?satoshis?的任意倍的數(shù)值,而不是BTC的數(shù)量。satoshis?是比特幣的百萬(wàn)分之一(0.00000001 BTC),因此這是比特幣中最小的貨幣單位(如1美分)。
satoshis:聰
鎖定腳本是一個(gè)放在一個(gè)輸出值上的“障礙”,同時(shí)它明確了今后花費(fèi)這筆輸出的條件。由于鎖定腳本往往含有一個(gè)公鑰(即比特幣地址),在歷史上它曾被稱作一個(gè)腳本公鑰代碼。在大多數(shù)比特幣應(yīng)用源代碼中,腳本公鑰代碼便是我們所說(shuō)的鎖定腳本。
由于我們還沒(méi)有實(shí)現(xiàn)錢包地址的邏輯,所以這里先暫且忽略鎖定腳本相關(guān)的邏輯。ScriptPubKey?將會(huì)存儲(chǔ)任意的字符串(用戶定義的錢包地址)
順便說(shuō)一句,擁有這樣的腳本語(yǔ)言意味著比特幣也可以用作智能合約平臺(tái)。
關(guān)于?交易輸出?的一個(gè)重要的事情是它們是不可分割的,這意味著你不能將它所存儲(chǔ)的數(shù)值拆開(kāi)來(lái)使用。當(dāng)這個(gè)交易輸出在新的交易中被交易輸入所引用時(shí),它將作為一個(gè)整體被花費(fèi)掉。 如果其值大于所需值,那么剩余的部分則會(huì)作為零錢返回給付款方。 這與真實(shí)世界的情況類似,例如,您支付5美元的鈔票用于購(gòu)買1美元的東西,那么你將會(huì)得到4美元的零錢。
交易輸入
/*** 交易輸入** @author wangwei* @date 2017/03/04*/ @Data @AllArgsConstructor @NoArgsConstructor public class TXInput {/*** 交易Id的hash值*/private byte[] txId;/*** 交易輸出索引*/private int txOutputIndex;/*** 解鎖腳本*/private String scriptSig;}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
前面提到過(guò),一個(gè)交易輸入指向的是某一筆交易的交易輸出:
- txId?存儲(chǔ)的是某筆交易的ID值
- txOutputIndex?存儲(chǔ)的是交易中這個(gè)交易輸出的索引位置(因?yàn)橐还P交易可能包含多個(gè)交易輸出)
- scriptSig?主要是提供用于交易輸出中?ScriptPubKey?所需的驗(yàn)證數(shù)據(jù)。?
- 如果這個(gè)數(shù)據(jù)被驗(yàn)證正確,那么相應(yīng)的交易輸出將被解鎖,并且其中的 value 能夠生成新的交易輸出;
- 如果不正確,那么相應(yīng)的交易輸出將不能被交易輸入所引用;
通過(guò)鎖定腳本與解鎖腳本這種機(jī)制,保證了某個(gè)用戶不能花費(fèi)屬于他人的Coins。
同樣,由于我們尚未實(shí)現(xiàn)錢包地址功能,ScriptSig?將會(huì)存儲(chǔ)任意的用戶所定義的錢包地址。我們將會(huì)在下一章節(jié)實(shí)現(xiàn)公鑰和數(shù)字簽名驗(yàn)證。
說(shuō)了這么多,我們來(lái)總結(jié)一下。交易輸出是”Coins”實(shí)際存儲(chǔ)的地方。每一個(gè)交易輸出都帶有一個(gè)鎖定腳本,它決定了解鎖的邏輯。每一筆新的交易必須至少有一個(gè)交易輸入與交易輸出。一筆交易的交易輸入指向前一筆交易的交易輸出,并且提供用于鎖定腳本解鎖需要的數(shù)據(jù)(ScriptSig?字段),然后利用交易輸出中的?value?去創(chuàng)建新的交易輸出。
注意,這段話的原文如下,但是里面有表述錯(cuò)誤的地方,交易輸出帶有的是鎖定腳本,而不是解鎖腳本。
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.
那到底是先有交易輸入還是先有交易輸出呢?
雞與蛋的問(wèn)題
在比特幣中,雞蛋先于雞出現(xiàn)。交易輸入源自于交易輸出的邏輯是典型的”先有雞還是先有蛋”的問(wèn)題:交易輸入產(chǎn)生交易輸出,交易輸出又會(huì)被交易輸入所引用。在比特幣中,交易輸出先于交易輸入出現(xiàn)。
當(dāng)?shù)V工開(kāi)始開(kāi)采區(qū)塊時(shí),區(qū)塊中會(huì)被添加一個(gè)?coinbase?交易。coinbase 交易是一種特殊的交易,它不需要以前已經(jīng)存在的交易輸出。它會(huì)憑空創(chuàng)建出交易輸出(i.e: Coins)。也即,雞蛋的出現(xiàn)并不需要母雞,這筆交易是作為礦工成功挖出新的區(qū)塊后的一筆獎(jiǎng)勵(lì)。
正如你所知道的那樣,在區(qū)塊鏈的最前端,即第一個(gè)區(qū)塊,有一個(gè)創(chuàng)世區(qū)塊。他產(chǎn)生了區(qū)塊鏈中有史以來(lái)的第一個(gè)交易輸出,并且由于沒(méi)有前一筆交易,也就沒(méi)有相應(yīng)的輸出,因此不需要前一筆交易的交易輸出。
讓我們來(lái)創(chuàng)建 coinbase 交易:
/*** 創(chuàng)建CoinBase交易** @param to 收賬的錢包地址* @param data 解鎖腳本數(shù)據(jù)* @return*/ public Transaction newCoinbaseTX(String to, String data) {if (StringUtils.isBlank(data)) {data = String.format("Reward to '%s'", to);}// 創(chuàng)建交易輸入TXInput txInput = new TXInput(new byte[]{}, -1, data);// 創(chuàng)建交易輸出TXOutput txOutput = new TXOutput(SUBSIDY, to);// 創(chuàng)建交易Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput});// 設(shè)置交易IDtx.setTxId();return tx; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
coinbase交易只有一個(gè)交易輸入。在我們的代碼實(shí)現(xiàn)中,txId?是空數(shù)組,txOutputIndex?設(shè)置為了 -1。另外,coinbase交易不會(huì)在?ScriptSig?字段上存儲(chǔ)解鎖腳本,相反,存了一個(gè)任意的數(shù)據(jù)。
在比特幣中,第一個(gè) coinbase 交易報(bào)刊了如下的信息:”The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”.?點(diǎn)擊查看
SUBSIDY?是挖礦獎(jiǎng)勵(lì)數(shù)量。在比特幣中,這個(gè)獎(jiǎng)勵(lì)數(shù)量沒(méi)有存儲(chǔ)在任何地方,而是依據(jù)現(xiàn)有區(qū)塊的總數(shù)進(jìn)行計(jì)算而得到:區(qū)塊總數(shù) 除以 210000。開(kāi)采創(chuàng)世區(qū)塊得到的獎(jiǎng)勵(lì)為50BTC,每過(guò) 210000 個(gè)區(qū)塊,獎(jiǎng)勵(lì)會(huì)減半。在我們的實(shí)現(xiàn)中,我們暫且將挖礦獎(jiǎng)勵(lì)設(shè)置為常數(shù)。(至少目前是這樣)
在區(qū)塊鏈中存儲(chǔ)交易信息
從現(xiàn)在開(kāi)始,每一個(gè)區(qū)塊必須存儲(chǔ)至少一個(gè)交易信息,并且盡可能地避免在沒(méi)有交易數(shù)據(jù)的情況下進(jìn)行挖礦。這意味著我們必須移除?Block?對(duì)象中的?date?字段,取而代之的是?transactions:
/*** 區(qū)塊** @author wangwei* @date 2018/02/02*/ @Data @AllArgsConstructor @NoArgsConstructor public class Block {/*** 區(qū)塊hash值*/private String hash;/*** 前一個(gè)區(qū)塊的hash值*/private String previousHash;/*** 交易信息*/private Transaction[] transactions;/*** 區(qū)塊創(chuàng)建時(shí)間(單位:秒)*/private long timeStamp;}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
相應(yīng)地,newGenesisBlock?與?newBlock?也都需要做改變:
/*** <p> 創(chuàng)建創(chuàng)世區(qū)塊 </p>** @param coinbase* @return*/ public static Block newGenesisBlock(Transaction coinbase) {return Block.newBlock("", new Transaction[]{coinbase}); }/*** <p> 創(chuàng)建新區(qū)塊 </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; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
接下來(lái),修改?newBlockchain?方法:
/*** <p> 創(chuàng)建區(qū)塊鏈 </p>** @param address 錢包地址* @return*/ public static Blockchain newBlockchain(String address) throws Exception {String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();if (StringUtils.isBlank(lastBlockHash)) {// 創(chuàng)建 coinBase 交易Transaction coinbaseTX = Transaction.newCoinbaseTX(address, "");Block genesisBlock = Block.newGenesisBlock(coinbaseTX);lastBlockHash = genesisBlock.getHash();RocksDBUtils.getInstance().putBlock(genesisBlock);RocksDBUtils.getInstance().putLastBlockHash(lastBlockHash);}return new Blockchain(lastBlockHash); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
現(xiàn)在,代碼有錢包地址的接口,將會(huì)收到開(kāi)采創(chuàng)世區(qū)塊的獎(jiǎng)勵(lì)。
工作量證明(Pow)
Pow算法必須將存儲(chǔ)在區(qū)塊中的交易信息考慮在內(nèi),以保存交易信息存儲(chǔ)的一致性和可靠性。因此,我們必須修改?ProofOfWork.prepareData?接口代碼邏輯:
/*** 準(zhǔn)備數(shù)據(jù)* <p>* 注意:在準(zhǔn)備區(qū)塊數(shù)據(jù)時(shí),一定要從原始數(shù)據(jù)類型轉(zhuǎn)化為byte[],不能直接從字符串進(jìn)行轉(zhuǎn)換* @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)); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
其中?hashTransaction?代碼如下:
/*** 對(duì)區(qū)塊中的交易信息進(jìn)行Hash計(jì)算** @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)); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
同樣,我們使用哈希值來(lái)作為數(shù)據(jù)的唯一標(biāo)識(shí)。我們希望區(qū)塊中的所有交易數(shù)據(jù)都能通過(guò)一個(gè)哈希值來(lái)定義它的唯一標(biāo)識(shí)。為了達(dá)到這個(gè)目的,我們計(jì)算了每一個(gè)交易的唯一哈希值,然后將他們串聯(lián)起來(lái),再對(duì)這個(gè)串聯(lián)后的組合進(jìn)行哈希值計(jì)算。
比特幣使用更復(fù)雜的技術(shù):它將所有包含在塊中的交易表示為?Merkle樹(shù)?,并在Proof-of-Work系統(tǒng)中使用該樹(shù)的根散列。 這種方法只需要跟節(jié)點(diǎn)的散列值就可以快速檢查塊是否包含某筆交易,而無(wú)需下載所有交易。
UTXO(未花費(fèi)交易輸出)
UTXO:unspend transaction output.(未被花費(fèi)的交易輸出)
在比特幣的世界里既沒(méi)有賬戶,也沒(méi)有余額,只有分散到區(qū)塊鏈里的UTXO.
UTXO?是理解比特幣交易原理的關(guān)鍵所在,我們先來(lái)看一段場(chǎng)景:
場(chǎng)景:假設(shè)你過(guò)去分別向A、B、C這三個(gè)比特幣用戶購(gòu)買了BTC,從A手中購(gòu)買了3.5個(gè)BTC,從B手中購(gòu)買了4.5個(gè)BTC,從C手中購(gòu)買了2個(gè)BTC,現(xiàn)在你的比特幣錢包里面恰好剩余10個(gè)BTC。
問(wèn)題:這個(gè)10個(gè)BTC是真正的10個(gè)BTC嗎?其實(shí)不是,這句話可能聽(tīng)起來(lái)有點(diǎn)怪。(什么!我錢包里面的BTC不是真正的BTC,你不要嚇我……)
解釋:前面提到過(guò)在比特幣的交易系統(tǒng)當(dāng)中,并不存在賬戶、余額這些概念,所以,你的錢包里面的10個(gè)BTC,并不是說(shuō)錢包余額為10個(gè)BTC。而是說(shuō),這10個(gè)BTC其實(shí)是由你的比特幣地址(錢包地址|公鑰)鎖定了的散落在各個(gè)區(qū)塊和各個(gè)交易里面的UTXO的總和。
UTXO 是比特幣交易的基本單位,每筆交易都會(huì)產(chǎn)生UTXO,一個(gè)UTXO可以是一“聰”的任意倍。給某人發(fā)送比特幣實(shí)際上是創(chuàng)造新的UTXO,綁定到那個(gè)人的錢包地址,并且能被他用于新的支付。
一般的比特幣交易由?交易輸入?和?交易輸出?兩部分組成。A向你支付3.5個(gè)BTC這筆交易,實(shí)際上產(chǎn)生了一個(gè)新的UTXO,這個(gè)新的UTXO 等于 3.5個(gè)BTC(3.5億聰),并且鎖定到了你的比特幣錢包地址上。
假如你要給你女(男)朋友轉(zhuǎn) 1.5 BTC,那么你的錢包會(huì)從可用的UTXO中選取一個(gè)或多個(gè)可用的個(gè)體來(lái)拼湊出一個(gè)大于或等于一筆交易所需的比特幣量。比如在這個(gè)假設(shè)場(chǎng)景里面,你的錢包會(huì)選取你和C的交易中的UTXO作為 交易輸入,input = 2BTC,這里會(huì)生成兩個(gè)新的交易輸出,一個(gè)輸出(UTXO = 1.5 BTC)會(huì)被綁定到你女(男)朋友的錢包地址上,另一個(gè)輸出(UTXO = 0.5 BTC)會(huì)作為找零,重新綁定到你的錢包地址上。
有關(guān)比特幣交易這部分更詳細(xì)的內(nèi)容,請(qǐng)查看:《精通比特幣(第二版)》第6章 —— 交易
我們需要找到所有未花費(fèi)的交易輸出(UTXO)。Unspent(未花費(fèi))?意味著這些交易輸出從未被交易輸入所指向。這前面的圖片中,UTXO如下:
當(dāng)然,當(dāng)我們檢查余額時(shí),我不需要區(qū)塊鏈中所有的UTXO,我只需要能被我們解鎖的UTXO(當(dāng)前,我們還沒(méi)有實(shí)現(xiàn)密鑰對(duì),而是替代為用戶自定義的錢包地址)。首先,我們?cè)诮灰纵斎肱c交易輸出上定義鎖定-解鎖的方法:
交易輸入:
public class TXInput {.../*** 判斷解鎖數(shù)據(jù)是否能夠解鎖交易輸出** @param unlockingData* @return*/public boolean canUnlockOutputWith(String unlockingData) {return this.getScriptSig().endsWith(unlockingData);} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
交易輸出:
public class TXOutput {.../*** 判斷解鎖數(shù)據(jù)是否能夠解鎖交易輸出** @param unlockingData* @return*/public boolean canBeUnlockedWith(String unlockingData) {return this.getScriptPubKey().endsWith(unlockingData);} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
這里我們暫時(shí)用?unlockingData?來(lái)與腳本字段進(jìn)行比較。我們會(huì)在后面的文章中來(lái)對(duì)這部分內(nèi)容進(jìn)行優(yōu)化,我們將會(huì)基于私鑰來(lái)實(shí)現(xiàn)用戶的錢包地址。
下一步,查詢所有與錢包地址綁定的包含UTXO的交易信息,有點(diǎn)復(fù)雜(本篇先這樣實(shí)現(xiàn),后面我們做一個(gè)與錢包地址映射的UTXO池來(lái)進(jìn)行優(yōu)化):
- 從與錢包地址對(duì)應(yīng)的交易輸入中查詢出所有已被花費(fèi)了的交易輸出
- 再來(lái)排除,尋找包含未被花費(fèi)的交易輸出的交易
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
得到了所有包含UTXO的交易數(shù)據(jù),接下來(lái),我們就可以得到所有UTXO集合了:
public class Blockchain {.../*** 查找錢包地址對(duì)應(yīng)的所有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;}...}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
現(xiàn)在,我們可以實(shí)現(xiàn)獲取錢包地址余額的接口了:
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);}...}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
查詢?wangwei?這個(gè)錢包地址的余額:
$ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei# 輸出 Balance of 'wangwei': 10- 1
- 2
- 3
- 4
轉(zhuǎn)賬
現(xiàn)在,我們想要給某人發(fā)送一些幣。因此,我們需要?jiǎng)?chuàng)建一筆新的交易,然后放入?yún)^(qū)塊中,再進(jìn)行挖礦。到目前為止,我們只是實(shí)現(xiàn)了?coinbase?交易,現(xiàn)在我們需要實(shí)現(xiàn)常見(jiàn)的創(chuàng)建交易接口:
public class Transaction {.../*** 從 from 向 to 支付一定的 amount 的金額** @param from 支付錢包地址* @param to 收款錢包地址* @param amount 交易金額* @param blockchain 區(qū)塊鏈* @return*/public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception {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;}...}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
在創(chuàng)建新的交易輸出之前,我們需要事先找到所有的UTXO,并確保有足夠的金額。這就是?findSpendableOutputs?要干的事情。之后,為每個(gè)找到的輸出創(chuàng)建一個(gè)引用它的輸入。接下來(lái),我們創(chuàng)建兩個(gè)交易輸出:
findSpendableOutputs?需要調(diào)用我們之前創(chuàng)建的?findUnspentTransactions?接口:
public class Blockchain {.../*** 尋找能夠花費(fèi)的交易** @param address 錢包地址* @param amount 花費(fèi)金額*/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);}...}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
這個(gè)方法會(huì)遍歷所有的UTXO并統(tǒng)計(jì)他們的總額。當(dāng)計(jì)算的總額恰好大于或者等于需要轉(zhuǎn)賬的金額時(shí),方法會(huì)停止遍歷,然后返回用于支付的總額以及按交易ID分組的交易輸出索引值數(shù)組。我們不想要花更多的錢。
現(xiàn)在,我們可以修改?Block.mineBlock?接口:
public class Block {.../*** 打包交易,進(jìn)行挖礦** @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);}...}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
最后,我們來(lái)實(shí)現(xiàn)轉(zhuǎn)賬的接口:
public class CLI {.../*** 轉(zhuǎn)賬** @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!");}...}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
轉(zhuǎn)賬,意味著創(chuàng)建一筆新的交易并且通過(guò)挖礦的方式將其存入?yún)^(qū)塊中。但是,比特幣不會(huì)像我們這樣做,它會(huì)把新的交易記錄先存到內(nèi)存池中,當(dāng)一個(gè)礦工準(zhǔn)備去開(kāi)采一個(gè)區(qū)塊時(shí),它會(huì)把打包內(nèi)存池中的所有交易信息,并且創(chuàng)建一個(gè)候選區(qū)塊。只有當(dāng)這個(gè)包含所有交易信息的候選區(qū)塊被成功開(kāi)采并且被添加到區(qū)塊鏈上時(shí),這些交易信息才算被確認(rèn)。
讓我們來(lái)測(cè)試一下:
# 先確認(rèn) wangwei 的余額 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei Balance of 'wangwei': 10# 轉(zhuǎn)賬 $ 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- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
贊!現(xiàn)在讓我們來(lái)創(chuàng)建更多的交易并且確保從多個(gè)交易輸出進(jìn)行轉(zhuǎn)賬是正常的:
$ 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!- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
現(xiàn)在,Helen 這個(gè)錢包地址上有了兩筆從 wangwei 和 Pedro 轉(zhuǎn)賬中產(chǎn)生的UTXO,讓我們將它們?cè)俎D(zhuǎn)賬給另外一個(gè)人:
$ 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- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
非常棒!讓我們來(lái)測(cè)試一下失敗的場(chǎng)景:
$ java -jar blockchain-java-jar-with-dependencies.jar send -from wangwei -to Ivan -amount 5 java.lang.Exception: ERROR: Not enough fundsat 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)- 1
- 2
- 3
- 4
- 5
- 6
總結(jié)
本篇內(nèi)容有點(diǎn)難度,但好歹我們現(xiàn)在有了交易信息了。盡管,缺少像比特幣這一類加密貨幣的一些關(guān)鍵特性:
資料
- 源代碼:https://github.com/wangweiX/blockchain-java/tree/part4-transaction1
- 《精通比特幣(第二版)》第6章 —— 交易
總結(jié)
以上是生活随笔為你收集整理的基于Java语言构建区块链(四)—— 交易(UTXO)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 基于Java语言构建区块链(五)—— 地
- 下一篇: 使用Java语言从零开始创建区块链