摘要:交易這一環(huán)節(jié)是整個(gè)比特幣系統(tǒng)當(dāng)中最為關(guān)鍵的一環(huán),并且區(qū)塊鏈唯一的目的就是通過(guò)安全的可信的方式來(lái)存儲(chǔ)交易信息,防止它們創(chuàng)建之后被人惡意篡改。在比特幣中,交易輸出先于交易輸入出現(xiàn)。
最終內(nèi)容請(qǐng)以原文為準(zhǔn): https://wangwei.one/posts/9cf...引言
上一篇 文章,我們實(shí)現(xiàn)了區(qū)塊數(shù)據(jù)的持久化,本篇開(kāi)始交易環(huán)節(jié)的實(shí)現(xiàn)。交易這一環(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)錢(qián)包所有者的敏感信息。Coins 不會(huì)匯總到錢(qián)包中。交易不會(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; }
一筆交易的 交易輸入 其實(shí)是指向上一筆交易的交易輸出 (這個(gè)后面詳細(xì)說(shuō)明)。我們錢(qián)包里面的 Coin(幣)實(shí)際是存儲(chǔ)在這些 交易輸出 里面。下圖表示了區(qū)塊鏈交易系統(tǒng)里面各個(gè)交易相互引用的關(guān)系:
注意:
有些 交易輸出 并不是由 交易輸入 產(chǎn)生,而是憑空產(chǎn)生的(后面會(huì)詳細(xì)介紹)。
但,交易輸入 必須指向某個(gè) 交易輸出,它不能憑空產(chǎn)生。
在一筆交易里面,交易輸入 可能會(huì)來(lái)自多筆交易所產(chǎn)生的 交易輸出。
在整篇文章中,我們將使用諸如“錢(qiá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; }
實(shí)際上,它表示的是能夠存儲(chǔ) "coins(幣)"的交易輸出(注意 value 字段)。并且這里所謂的 value 實(shí)際上是由存儲(chǔ)在 ScriptPubKey (鎖定腳本)中的一個(gè)puzzle(難題) 所鎖定。在內(nèi)部,比特幣使用稱(chēng)為腳本的腳本語(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è)公鑰(即比特幣地址),在歷史上它曾被稱(chēng)作一個(gè)腳本公鑰代碼。在大多數(shù)比特幣應(yīng)用源代碼中,腳本公鑰代碼便是我們所說(shuō)的鎖定腳本。
由于我們還沒(méi)有實(shí)現(xiàn)錢(qián)包地址的邏輯,所以這里先暫且忽略鎖定腳本相關(guān)的邏輯。ScriptPubKey 將會(huì)存儲(chǔ)任意的字符串(用戶定義的錢(qián)包地址)
順便說(shuō)一句,擁有這樣的腳本語(yǔ)言意味著比特幣也可以用作智能合約平臺(tái)。
關(guān)于 交易輸出 的一個(gè)重要的事情是它們是不可分割的,這意味著你不能將它所存儲(chǔ)的數(shù)值拆開(kāi)來(lái)使用。當(dāng)這個(gè)交易輸出在新的交易中被交易輸入所引用時(shí),它將作為一個(gè)整體被花費(fèi)掉。 如果其值大于所需值,那么剩余的部分則會(huì)作為零錢(qián)返回給付款方。 這與真實(shí)世界的情況類(lèi)似,例如,您支付5美元的鈔票用于購(gòu)買(mǎi)1美元的東西,那么你將會(huì)得到4美元的零錢(qián)。
交易輸入/** * 交易輸入 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class TXInput { /** * 交易Id的hash值 */ private byte[] txId; /** * 交易輸出索引 */ private int txOutputIndex; /** * 解鎖腳本 */ private String scriptSig; }
前面提到過(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)錢(qián)包地址功能,ScriptSig 將會(huì)存儲(chǔ)任意的用戶所定義的錢(qián)包地址。我們將會(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 收賬的錢(qián)包地址 * @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è)置交易ID tx.setTxId(); return tx; }
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; }
相應(yīng)地,newGenesisBlock 與 newBlock 也都需要做改變:
/** *創(chuàng)建創(chuàng)世區(qū)塊
* * @param coinbase * @return */ public static Block newGenesisBlock(Transaction coinbase) { return Block.newBlock("", new Transaction[]{coinbase}); } /** *創(chuàng)建新區(qū)塊
* * @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; }
接下來(lái),修改 newBlockchain 方法:
/** *創(chuàng)建區(qū)塊鏈
* * @param address 錢(qián)包地址 * @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); }
現(xiàn)在,代碼有錢(qián)包地址的接口,將會(huì)收到開(kāi)采創(chuàng)世區(qū)塊的獎(jiǎng)勵(lì)。
工作量證明(Pow)Pow算法必須將存儲(chǔ)在區(qū)塊中的交易信息考慮在內(nèi),以保存交易信息存儲(chǔ)的一致性和可靠性。因此,我們必須修改 ProofOfWork.prepareData 接口代碼邏輯:
/** * 準(zhǔn)備數(shù)據(jù) ** 注意:在準(zhǔn)備區(qū)塊數(shù)據(jù)時(shí),一定要從原始數(shù)據(jù)類(lèi)型轉(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) ); }
其中 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)); }
同樣,我們使用哈希值來(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)買(mǎi)了BTC,從A手中購(gòu)買(mǎi)了3.5個(gè)BTC,從B手中購(gòu)買(mǎi)了4.5個(gè)BTC,從C手中購(gòu)買(mǎi)了2個(gè)BTC,現(xiàn)在你的比特幣錢(qián)包里面恰好剩余10個(gè)BTC。
問(wèn)題:這個(gè)10個(gè)BTC是真正的10個(gè)BTC嗎?其實(shí)不是,這句話可能聽(tīng)起來(lái)有點(diǎn)怪。(什么!我錢(qián)包里面的BTC不是真正的BTC,你不要嚇我……)
解釋?zhuān)呵懊嫣岬竭^(guò)在比特幣的交易系統(tǒng)當(dāng)中,并不存在賬戶、余額這些概念,所以,你的錢(qián)包里面的10個(gè)BTC,并不是說(shuō)錢(qián)包余額為10個(gè)BTC。而是說(shuō),這10個(gè)BTC其實(shí)是由你的比特幣地址(錢(qián)包地址|公鑰)鎖定了的散落在各個(gè)區(qū)塊和各個(gè)交易里面的UTXO的總和。
UTXO 是比特幣交易的基本單位,每筆交易都會(huì)產(chǎn)生UTXO,一個(gè)UTXO可以是一“聰”的任意倍。給某人發(fā)送比特幣實(shí)際上是創(chuàng)造新的UTXO,綁定到那個(gè)人的錢(qián)包地址,并且能被他用于新的支付。
一般的比特幣交易由 交易輸入 和 交易輸出 兩部分組成。A向你支付3.5個(gè)BTC這筆交易,實(shí)際上產(chǎn)生了一個(gè)新的UTXO,這個(gè)新的UTXO 等于 3.5個(gè)BTC(3.5億聰),并且鎖定到了你的比特幣錢(qián)包地址上。
假如你要給你女(男)朋友轉(zhuǎn) 1.5 BTC,那么你的錢(qián)包會(huì)從可用的UTXO中選取一個(gè)或多個(gè)可用的個(gè)體來(lái)拼湊出一個(gè)大于或等于一筆交易所需的比特幣量。比如在這個(gè)假設(shè)場(chǎng)景里面,你的錢(qián)包會(huì)選取你和C的交易中的UTXO作為 交易輸入,input = 2BTC,這里會(huì)生成兩個(gè)新的交易輸出,一個(gè)輸出(UTXO = 1.5 BTC)會(huì)被綁定到你女(男)朋友的錢(qián)包地址上,另一個(gè)輸出(UTXO = 0.5 BTC)會(huì)作為找零,重新綁定到你的錢(qián)包地址上。
有關(guān)比特幣交易這部分更詳細(xì)的內(nèi)容,請(qǐng)查看:《精通比特幣(第二版)》第6章 —— 交易
我們需要找到所有未花費(fèi)的交易輸出(UTXO)。Unspent(未花費(fèi)) 意味著這些交易輸出從未被交易輸入所指向。這前面的圖片中,UTXO如下:
tx0, output 1;
tx1, output 0;
tx3, output 0;
tx4, output 0.
當(dāng)然,當(dāng)我們檢查余額時(shí),我不需要區(qū)塊鏈中所有的UTXO,我只需要能被我們解鎖的UTXO(當(dāng)前,我們還沒(méi)有實(shí)現(xiàn)密鑰對(duì),而是替代為用戶自定義的錢(qián)包地址)。首先,我們?cè)诮灰纵斎肱c交易輸出上定義鎖定-解鎖的方法:
交易輸入:
public class TXInput { ... /** * 判斷解鎖數(shù)據(jù)是否能夠解鎖交易輸出 * * @param unlockingData * @return */ public boolean canUnlockOutputWith(String unlockingData) { return this.getScriptSig().endsWith(unlockingData); } }
交易輸出:
public class TXOutput { ... /** * 判斷解鎖數(shù)據(jù)是否能夠解鎖交易輸出 * * @param unlockingData * @return */ public boolean canBeUnlockedWith(String unlockingData) { return this.getScriptPubKey().endsWith(unlockingData); } }
這里我們暫時(shí)用 unlockingData 來(lái)與腳本字段進(jìn)行比較。我們會(huì)在后面的文章中來(lái)對(duì)這部分內(nèi)容進(jìn)行優(yōu)化,我們將會(huì)基于私鑰來(lái)實(shí)現(xiàn)用戶的錢(qián)包地址。
下一步,查詢所有與錢(qián)包地址綁定的包含UTXO的交易信息,有點(diǎn)復(fù)雜(本篇先這樣實(shí)現(xiàn),后面我們做一個(gè)與錢(qián)包地址映射的UTXO池來(lái)進(jìn)行優(yōu)化):
從與錢(qián)包地址對(duì)應(yīng)的交易輸入中查詢出所有已被花費(fèi)了的交易輸出
再來(lái)排除,尋找包含未被花費(fèi)的交易輸出的交易
public class Blockchain { ... /** * 查找錢(qián)包地址對(duì)應(yīng)的所有未花費(fèi)的交易 * * @param address 錢(qián)包地址 * @return */ private Transaction[] findUnspentTransactions(String address) throws Exception { MapallSpentTXOs = this.getAllSpentTXOs(address); Transaction[] unspentTxs = {}; // 再次遍歷所有區(qū)塊中的交易輸出 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; } /** * 從交易輸入中查詢區(qū)塊鏈中所有已被花費(fèi)了的交易輸出 * * @param address 錢(qián)包地址 * @return 交易ID以及對(duì)應(yīng)的交易輸出下標(biāo)地址 * @throws Exception */ private Map getAllSpentTXOs(String address) throws Exception { // 定義TxId ——> spentOutIndex[],存儲(chǔ)交易ID與已被花費(fèi)的交易輸出數(shù)組索引值 Map spentTXOs = new HashMap<>(); for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { Block block = blockchainIterator.next(); for (Transaction transaction : block.getTransactions()) { // 如果是 coinbase 交易,直接跳過(guò),因?yàn)樗淮嬖谝们耙粋€(gè)區(qū)塊的交易輸出 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的交易數(shù)據(jù),接下來(lái),我們就可以得到所有UTXO集合了:
public class Blockchain { ... /** * 查找錢(qián)包地址對(duì)應(yīng)的所有UTXO * * @param address 錢(qián)包地址 * @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; } ... }
現(xiàn)在,我們可以實(shí)現(xiàn)獲取錢(qián)包地址余額的接口了:
public class CLI { ... /** * 查詢錢(qián)包余額 * * @param address 錢(qián)包地址 */ 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 這個(gè)錢(qián)包地址的余額:
$ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address wangwei # 輸出 Balance of "wangwei": 10轉(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 支付錢(qián)包地址 * @param to 收款錢(qián)包地址 * @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(); 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; } ... }
在創(chuàng)建新的交易輸出之前,我們需要事先找到所有的UTXO,并確保有足夠的金額。這就是 findSpendableOutputs 要干的事情。之后,為每個(gè)找到的輸出創(chuàng)建一個(gè)引用它的輸入。接下來(lái),我們創(chuàng)建兩個(gè)交易輸出:
一個(gè) output 用于鎖定到接收者的錢(qián)包地址上。這個(gè)是真正被轉(zhuǎn)走的coins;
另一個(gè) output 鎖定到發(fā)送者的錢(qián)包地址上。這個(gè)就是 找零。只有當(dāng)用于支付的UTXO總和大于要支付的金額時(shí),才會(huì)創(chuàng)建這部分的 交易輸出。記?。航灰纵敵鍪?strong>不可分割的
findSpendableOutputs 需要調(diào)用我們之前創(chuàng)建的 findUnspentTransactions 接口:
public class Blockchain { ... /** * 尋找能夠花費(fèi)的交易 * * @param address 錢(qián)包地址 * @param amount 花費(fèi)金額 */ 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); } ... }
這個(gè)方法會(huì)遍歷所有的UTXO并統(tǒng)計(jì)他們的總額。當(dāng)計(jì)算的總額恰好大于或者等于需要轉(zhuǎn)賬的金額時(shí),方法會(huì)停止遍歷,然后返回用于支付的總額以及按交易ID分組的交易輸出索引值數(shù)組。我們不想要花更多的錢(qián)。
現(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); } ... }
最后,我們來(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!"); } ... }
轉(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
贊!現(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!
現(xiàn)在,Helen 這個(gè)錢(qián)包地址上有了兩筆從 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
非常棒!讓我們來(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 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)總結(jié)
本篇內(nèi)容有點(diǎn)難度,但好歹我們現(xiàn)在有了交易信息了。盡管,缺少像比特幣這一類(lèi)加密貨幣的一些關(guān)鍵特性:
錢(qián)包地址。我們還沒(méi)有基于私鑰的真實(shí)地址。
獎(jiǎng)勵(lì)。挖礦絕對(duì)沒(méi)有利潤(rùn)。
UTXO集。當(dāng)我們計(jì)算錢(qián)包地址的余額時(shí),我們需要遍歷所有的區(qū)塊中的所有交易信息,當(dāng)有許許多多的區(qū)塊時(shí),這將花費(fèi)不少的時(shí)間。此外,如果我們想驗(yàn)證以后的交易,可能需要很長(zhǎng)時(shí)間。 UTXO集旨在解決這些問(wèn)題并快速處理交易。
內(nèi)存池。 這是交易在打包成區(qū)塊之前存儲(chǔ)的地方。 在我們當(dāng)前的實(shí)現(xiàn)中,一個(gè)塊只包含一筆交易,而且效率很低。
資料源代碼:https://github.com/wangweiX/b...
《精通比特幣(第二版)》第6章 —— 交易
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/76356.html
摘要:交易這一環(huán)節(jié)是整個(gè)比特幣系統(tǒng)當(dāng)中最為關(guān)鍵的一環(huán),并且區(qū)塊鏈唯一的目的就是通過(guò)安全的可信的方式來(lái)存儲(chǔ)交易信息,防止它們創(chuàng)建之后被人惡意篡改。在比特幣中,交易輸出先于交易輸入出現(xiàn)。 showImg(https://segmentfault.com/img/remote/1460000013923562?w=3200&h=1400); 最終內(nèi)容請(qǐng)以原文為準(zhǔn): https://wangwei....
摘要:截止年月號(hào),比特幣中有個(gè)區(qū)塊,并且這些數(shù)據(jù)占據(jù)了的磁盤(pán)空間。每個(gè)比特幣節(jié)點(diǎn)都是路由區(qū)塊鏈數(shù)據(jù)庫(kù)挖礦錢(qián)包服務(wù)的功能集合。是比特幣的輕量級(jí)節(jié)點(diǎn),它不需要下載所有的區(qū)塊鏈數(shù)據(jù),也不需要驗(yàn)證區(qū)塊和交易數(shù)據(jù)。 showImg(https://img.i7years.com/blog/pexels-photo-38136.jpeg); 最終內(nèi)容請(qǐng)以原文為準(zhǔn):https://wangwei.one/...
摘要:截止年月號(hào),比特幣中有個(gè)區(qū)塊,并且這些數(shù)據(jù)占據(jù)了的磁盤(pán)空間。每個(gè)比特幣節(jié)點(diǎn)都是路由區(qū)塊鏈數(shù)據(jù)庫(kù)挖礦錢(qián)包服務(wù)的功能集合。是比特幣的輕量級(jí)節(jié)點(diǎn),它不需要下載所有的區(qū)塊鏈數(shù)據(jù),也不需要驗(yàn)證區(qū)塊和交易數(shù)據(jù)。 showImg(https://img.i7years.com/blog/pexels-photo-38136.jpeg); 最終內(nèi)容請(qǐng)以原文為準(zhǔn):https://wangwei.one/...
摘要:我們目前正處于一個(gè)新興的區(qū)塊鏈開(kāi)發(fā)行業(yè)中。,一種在以太坊開(kāi)發(fā)人員中流行的新的簡(jiǎn)單編程語(yǔ)言,因?yàn)樗怯糜陂_(kāi)發(fā)以太坊智能合約的語(yǔ)言。它是全球至少萬(wàn)開(kāi)發(fā)人員使用的世界上最流行的編程語(yǔ)言之一。以太坊,主要是針對(duì)工程師使用進(jìn)行區(qū)塊鏈以太坊開(kāi)發(fā)的詳解。 我們目前正處于一個(gè)新興的區(qū)塊鏈開(kāi)發(fā)行業(yè)中。區(qū)塊鏈技術(shù)處于初期階段,然而這種顛覆性技術(shù)已經(jīng)成功地風(fēng)靡全球,并且最近經(jīng)歷了一場(chǎng)與眾不同的繁榮。由于許多...
摘要:我們?cè)撨x擇哪一款數(shù)據(jù)庫(kù)呢事實(shí)上,在比特幣白皮書(shū)中并沒(méi)有明確指定使用哪一種的數(shù)據(jù)庫(kù),因此這個(gè)由開(kāi)發(fā)人員自己決定。詳見(jiàn)精通比特幣第二版第章節(jié)交易的輸入與輸出此外,每個(gè)區(qū)塊數(shù)據(jù)都是以單獨(dú)的文件形式存儲(chǔ)在磁盤(pán)上。資料源代碼精通比特幣第二版 showImg(https://segmentfault.com/img/remote/1460000013923488?w=1200&h=627); 最...
閱讀 3829·2021-10-12 10:11
閱讀 3648·2021-09-13 10:27
閱讀 2555·2019-08-30 15:53
閱讀 1983·2019-08-29 18:33
閱讀 2198·2019-08-29 14:03
閱讀 1004·2019-08-29 13:27
閱讀 3327·2019-08-28 18:07
閱讀 796·2019-08-26 13:23