最終內容請以原文爲準: https://wangwei.one/posts/f90...
在 上一篇 文章當中,咱們開始了交易機制的實現。你已經瞭解到交易的一些非我的特徵:沒有用戶帳戶,您的我的數據(例如:姓名、護照號碼以及SSN(美國社會安全卡(Social Security Card)上的9 位數字))不是必需的,而且不存儲在比特幣的任何地方。但仍然必須有一些東西可以識別你是這些交易輸出的全部者(例如:鎖定在這些輸出上的幣的全部者)。這就是比特幣地址的做用所在。到目前爲止,咱們只是使用了任意的用戶定義的字符串當作地址,如今是時候來實現真正的地址了,就像它們在比特幣中實現的同樣。php
<!--more-->html
這裏有一個比特幣地址的示例:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。這是一個很是早期的比特幣地址,據稱是屬於中本聰的比特幣地址。比特幣地址是公開的。若是你想要給某人發送比特幣,你須要知道對方的比特幣地址。可是地址(儘管它是惟一的)並不能做爲你是一個錢包全部者的憑證。事實上,這樣的地址是公鑰的一種可讀性更好的表示 。在比特幣中,你的身份是存儲在你計算機上(或存儲在你有權訪問的其餘位置)的一對(或多對)私鑰和公鑰。比特幣依靠加密算法的組合來建立這些密鑰,並保證世界上沒有其餘人任何人能夠在沒有物理訪問密鑰的狀況下訪問您的比特幣。java
比特幣地址與公鑰不一樣。比特幣地址是由公鑰通過單向的哈希函數生成的
接下來,讓咱們來討論一下這些加密算法。git
注意:不要向本篇文章中的代碼所生成的任何比特幣地址發送真實的比特幣來進行測試,不然後果自負……
公鑰加密算法(public-key cryptography)使用的是密鑰對:公鑰和私鑰。公鑰屬於非敏感信息,能夠向任何人透露。相比之下,私鑰不能公開披露:除了全部者以外,任何人都不能擁有私鑰的權限,由於它是用做全部者標識的私鑰。你的私鑰表明就是你(固然是在加密貨幣世界裏的)。github
本質上,比特幣錢包就是一對這樣的密鑰。當你安裝一個錢包應用程序或者使用比特幣客戶端去生成一個新的地址時,它們就爲你建立好了一個密鑰對。在比特幣種,誰控制了私鑰,誰就掌握了全部發往對應公鑰地址上全部比特幣的控制權。算法
私鑰和公鑰只是隨機的字節序列,所以它們不能被打印在屏幕上供人讀取。這就是爲何比特幣會用一種算法將公鑰的字節序列轉化爲人類可讀的字符串形式。shell
若是你曾今使用過比特幣錢包的應用程序,它可能會爲你生成助記詞密碼短語。這些助記詞能夠用來替代私鑰,而且可以生成私鑰。這種機制是經過 BIP-039 來實現的。
好了,如今咱們已經知道在比特幣中由什麼來決定用戶的標識了。可是,比特幣是如何校驗交易輸出(和它裏面存儲的一些幣)的全部權的呢?編程
在數學和密碼學中,有個數字簽名的概念,這套算法保證瞭如下幾點:數組
經過對數據應用簽名算法(即簽署數據),能夠獲得一個簽名,之後能夠對其進行驗證。數字簽名須要使用私鑰,而驗證則須要公鑰。安全
爲了可以簽署數據咱們須要:
簽名操做會產生一個存儲在交易輸入中的簽名。爲了可以驗證一個簽名,咱們須要:
簡單來說,這個驗證的過程能夠被描述爲:檢查簽名是由被簽名數據加上私鑰得來,而且這個公鑰也是由該私鑰生成。
數字簽名並非一種加密方法,你沒法從簽名反向構造出源數據。這個和咱們 前面 提到過的Hash算法有點相似:經過對一個數據使用Hash算法,你能夠獲得該數據的惟一表示。它們二者的不一樣之處在於,簽名算法多了一個密鑰對:它讓數字簽名得以驗證成爲可能。可是密鑰對也可以用於去加密數據:私鑰用於加密數據,公鑰用於解密數據。不過比特幣並無使用加密算法。
在比特幣中,每一筆交易輸入都會被該筆交易的建立者進行簽名。比特幣中的每一筆交易在放入區塊以前都必須獲得驗證。驗證的意思就是:
數據簽名以及簽名驗證的過程以下圖所示:
讓咱們來回顧一下交易的完整生命週期:
RIPEMD16(SHA256(PubKey))
)當比特幣網絡中的其餘節點收到其餘節點廣播的交易數據以後將,將會對其進行驗證。其餘的事情除外,他們將會驗證:
正如前面所提到的那樣,公鑰和私鑰是一串隨機的字符序列。因爲私鑰是用來識別比特幣全部者身份的緣故,所以有一個必要的條件:這個隨機算法必須產生真正的隨機序列。咱們不但願意外地生成其餘人所擁有的私鑰。也就是要保證隨機序列的絕對惟一性。
比特幣是使用的橢圓曲線來生成的私鑰。橢圓曲線是一個很是複雜的數學概念,這裏咱們不作詳細的介紹(若是你對此很是好奇,能夠點擊 this gentle introduction to elliptic curves 進行詳細的 瞭解,警告:數學公式)。咱們須要知道的是,這些曲線能夠用來生成真正大而隨機的數字。比特幣所採用的曲線算法可以隨機生成一個介於0到 2^2^56之間的數字(這是一個很是大的數字,用十進制表示的話,大約是10^77, 而整個可見的宇宙中,原子數在 10^78 到 10^82 之間) 。這麼巨大的上限意味着產生兩個同樣的私鑰是幾乎不可能的事情。
另外,咱們將會使用比特幣中所使用的 ECDSA (橢圓曲線數字簽名算法)去簽署交易信息。
如今讓咱們回到上面提到的比特幣地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa . 如今咱們知道這個地址實際上是公鑰的一種可讀高的表示方式。若是咱們對他進行解碼,咱們會看到公鑰看起來是這樣子的(字節序列的十六進制的表示方式):
0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93
Base64使用了26個小寫字母、26個大寫字母、10個數字以及兩個符號(例如「+」和「/」),用於在電子郵件這樣的基於文本的媒介中傳輸二進制數據。Base64一般用於編碼郵件中的附件。Base58是一種基於文本的二進制編碼格式,用在比特幣和其它的加密貨幣中。這種編碼格式不只實現了數據壓縮,保持了易讀性,還具備錯誤診斷功能。Base58是Base64編碼格式的子集,一樣使用大小寫字母和10個數字,但捨棄了一些容易錯讀和在特定字體中容易混淆的字符。具體地,Base58不含Base64中的0(數字0)、O(大寫字母o)、l(小寫字母L)、I(大寫字母i),以及「+」和「/」兩個字符。簡而言之,Base58就是由不包括(0,O,l,I)的大小寫字母和數字組成。
比特幣的Base58字母表:
123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
Base58Check是一種經常使用在比特幣中的Base58編碼格式,增長了錯誤校驗碼來檢查數據在轉錄中出現的錯誤。校驗碼長4個字節,添加到須要編碼的數據以後。校驗碼是從須要編碼的數據的哈希值中獲得的,因此能夠用來檢測並避免轉錄和輸入中產生的錯誤。使用Base58check編碼格式時,編碼軟件會計算原始數據的校驗碼並和結果數據中自帶的校驗碼進行對比。兩者不匹配則代表有錯誤產生,那麼這個Base58Check格式的數據就是無效的。例如,一個錯誤比特幣地址就不會被錢包認爲是有效的地址,不然這種錯誤會形成資金的丟失。
爲了使用Base58Check編碼格式對數據(數字)進行編碼,首先咱們要對數據添加一個稱做「版本字節」的前綴,這個前綴用來明確須要編碼的數據的類型。例如,比特幣地址的前綴是0(十六進制是0x00),而對私鑰編碼時前綴是128(十六進制是0x80)。
讓咱們以示意圖的形式展現一下從公鑰獲得地址的過程:
所以,上述解碼的公鑰由三部分組成:
Version Public key hash Checksum 00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93
因爲哈希函數是單向的(也就說沒法逆轉回去),因此不可能從一個哈希中提取公鑰。不過經過執行哈希函數並進行哈希比較,咱們能夠檢查一個公鑰是否被用於哈希的生成。
OK,如今咱們有了全部的東西,讓咱們來編寫一些代碼。 當一些概念被寫成代碼時,咱們會對此理解的更加清晰和深入。
讓咱們從 Wallet 的構成開始,這裏咱們須要先引入一個maven包:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.59</version> </dependency>
錢包結構
/** * 錢包 * * @author wangwei * @date 2018/03/14 */ @Data @AllArgsConstructor public class Wallet { // 校驗碼長度 private static final int ADDRESS_CHECKSUM_LEN = 4; /** * 私鑰 */ private BCECPrivateKey privateKey; /** * 公鑰 */ private byte[] publicKey; public Wallet() { initWallet(); } /** * 初始化錢包 */ private void initWallet() { try { KeyPair keyPair = newECKeyPair(); BCECPrivateKey privateKey = (BCECPrivateKey) keyPair.getPrivate(); BCECPublicKey publicKey = (BCECPublicKey) keyPair.getPublic(); byte[] publicKeyBytes = publicKey.getQ().getEncoded(false); this.setPrivateKey(privateKey); this.setPublicKey(publicKeyBytes); } catch (Exception e) { e.printStackTrace(); } } /** * 建立新的密鑰對 * * @return * @throws Exception */ private KeyPair newKeyPair() throws Exception { // 註冊 BC Provider Security.addProvider(new BouncyCastleProvider()); // 建立橢圓曲線算法的密鑰對生成器,算法爲 ECDSA KeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); // 橢圓曲線(EC)域參數設定 // bitcoin 爲何會選擇 secp256k1,詳見:https://bitcointalk.org/index.php?topic=151120.0 ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); g.initialize(ecSpec, new SecureRandom()); return g.generateKeyPair(); } }
所謂的錢包,其實本質上就是一個密鑰對。這裏咱們須要藉助 KeyPairGenerator 生成密鑰對。
接着,咱們來生成比特幣的錢包地址:
public class Wallet { ... /** * 獲取錢包地址 * * @return */ public String getAddress() throws Exception { // 1. 獲取 ripemdHashedKey byte[] ripemdHashedKey = BtcAddressUtils.ripeMD160Hash(this.getPublicKey().getEncoded()); // 2. 添加版本 0x00 ByteArrayOutputStream addrStream = new ByteArrayOutputStream(); addrStream.write((byte) 0); addrStream.write(ripemdHashedKey); byte[] versionedPayload = addrStream.toByteArray(); // 3. 計算校驗碼 byte[] checksum = BtcAddressUtils.checksum(versionedPayload); // 4. 獲得 version + paylod + checksum 的組合 addrStream.write(checksum); byte[] binaryAddress = addrStream.toByteArray(); // 5. 執行Base58轉換處理 return Base58Check.rawBytesToBase58(binaryAddress); } ... }
這個時候,你就能夠獲得 真實的比特幣地址 了,而且你能夠到 blockchain.info 上去檢查這個地址的餘額。
例如,經過 getAddress 方法,獲得了一個比特幣地址爲: 1rZ9SjXMRwnbW3Pu8itC1HtNBVHERSQhaACbL16
我敢保證,不管你生成多少次比特幣地址,它的餘額始終爲0.這就是爲何選擇適當的公鑰密碼算法如此重要:考慮到私鑰是隨機數字,產生相同數字的機會必須儘量低。 理想狀況下,它必須低至「永不」。
另外,須要注意的是你不須要鏈接到比特幣的節點上去獲取比特幣的地址。有關地址生成的開源算法工具包已經有不少編程語言和庫實現了。
如今,咱們須要去修改交易輸入與輸出,讓他們開始使用真實的地址:
/** * 交易輸入 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class TXInput { /** * 交易Id的hash值 */ private byte[] txId; /** * 交易輸出索引 */ private int txOutputIndex; /** * 簽名 */ private byte[] signature; /** * 公鑰 */ private byte[] pubKey; /** * 檢查公鑰hash是否用於交易輸入 * * @param pubKeyHash * @return */ public boolean usesKey(byte[] pubKeyHash) { byte[] lockingHash = BtcAddressUtils.ripeMD160Hash(this.getPubKey()); return Arrays.equals(lockingHash, pubKeyHash); } }
/** * 交易輸出 * * @author wangwei * @date 2017/03/04 */ @Data @AllArgsConstructor @NoArgsConstructor public class TXOutput { /** * 數值 */ private int value; /** * 公鑰Hash */ private byte[] pubKeyHash; /** * 建立交易輸出 * * @param value * @param address * @return */ public static TXOutput newTXOutput(int value, String address) { // 反向轉化爲 byte 數組 byte[] versionedPayload = Base58Check.base58ToBytes(address); byte[] pubKeyHash = Arrays.copyOfRange(versionedPayload, 1, versionedPayload.length); return new TXOutput(value, pubKeyHash); } /** * 檢查交易輸出是否可以使用指定的公鑰 * * @param pubKeyHash * @return */ public boolean isLockedWithKey(byte[] pubKeyHash) { return Arrays.equals(this.getPubKeyHash(), pubKeyHash); } }
代碼中還有不少其餘的地方須要變更,這裏不一一指出,詳見文末的源碼鏈接。
注意,因爲咱們不會去實現腳本語言特性,因此咱們再也不使用 scriptPubKey 和 scriptSig 字段。取而代之的是,咱們將 scriptSig 拆分爲了 signature 和 pubKey 字段,scriptPubKey 重命名爲了 pubKeyHash 。咱們將會實現相似於比特幣中的交易輸出鎖定/解鎖邏輯和交易輸入的簽名邏輯,可是咱們會在方法中執行此操做。
usesKey 用於檢查交易輸入中的公鑰是否可以解鎖交易輸出。須要注意的是,交易輸入中存儲的是未經hash過的公鑰,可是方法實現中對它作了一步 ripeMD160Hash
轉化。
isLockedWithKey 用於檢查提供的公鑰Hash是否可以用於解鎖交易輸出,這個方法是 usesKey 的補充。usesKey 被用於 getAllSpentTXOs 方法中,isLockedWithKey 被用於 findUnspentTransactions 方法中,這樣使得在先後兩筆交易之間創建起了鏈接。
newTXOutput 方法中,將 value 鎖定到了 address 上。當咱們向別人發送比特幣時,咱們只知道他們的地址,所以函數將地址做爲惟一的參數。而後解碼地址,並從中提取公鑰哈希並保存在PubKeyHash字段中。
如今,讓咱們一塊兒來檢查一下是否可以正常運行:
$ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh $ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e $ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1 $ java -jar blockchain-java-jar-with-dependencies.jar createblockchain -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh Elapsed Time: 6.77 seconds correct hash Hex: 00000e44be0c94c39a4fef24c67d85c428e8bfbd227e292d75c0f4d398e2e81c Done ! $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh Balance of '13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh': 10 $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e -to 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVd -amount 5 java.lang.Exception: ERROR: Not enough funds $ java -jar blockchain-java-jar-with-dependencies.jar send -from 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh -to 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e-amount 5 Elapsed Time: 4.477 seconds correct hash Hex: 00000da41dfacc8032a553ed5b1aa5e24318d5d89ca14a16c4f70129609c8365 Success! $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh Balance of '13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh': 5 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e Balance of '1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e': 5 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1 Balance of '19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1': 0
Nice! 如今讓咱們一塊兒來實現交易簽名部分的內容。
交易數據必須被簽名,由於這是比特幣中可以保證不能花費屬於他人比特幣的惟一方法。若是一個簽名是無效的,那麼這筆交易也是無效的,這樣的話,這筆交易就不能被添加到區塊鏈中去。
咱們已經有了實現交易簽名的全部片斷,還有一個事情除外:用於簽名的數據。交易數據中哪一部分是真正用於簽名的呢?難道是所有數據?選擇用於簽名的數據至關的重要。用於簽名的數據必須包含以獨特且惟一的方式標識數據的信息。例如,僅對交易輸出簽名是沒有意義的,由於此簽名不會考慮發送發與接收方。
考慮到交易數據要解鎖前面的交易輸出,從新分配交易輸出中的 value 值,而且鎖定新的交易輸出,所以下面這些數據是必須被簽名的:
在比特幣中,鎖定/解鎖邏輯存儲在腳本中,解鎖腳本存儲在交易輸入的 ScriptSig 字段中,而鎖定腳本存儲在交易輸出的 ScriptPubKey 的字段中。 因爲比特幣容許不一樣類型的腳本,所以它會對ScriptPubKey的所有內容進行簽名。
如你所見,咱們不須要去對存儲在交易輸入中的公鑰進行簽名。正由於如此,在比特幣中,所簽名的並非一個交易,而是一個去除部份內容的交易輸入副本,交易輸入裏面存儲了被引用交易輸出的 ScriptPubKey
。
獲取修剪後的交易副本的詳細過程在 這裏. 雖然它可能已通過時了,可是我並無找到另外一個更可靠的來源。
OK,它看起來有點複雜,所以讓咱們來開始coding吧。咱們將從 Sign 方法開始:
public class Transaction { ... /** * 簽名 * * @param privateKey 私鑰 * @param prevTxMap 前面多筆交易集合 */ public void sign(BCECPrivateKey privateKey, Map<String, Transaction> prevTxMap) throws Exception { // coinbase 交易信息不須要簽名,由於它不存在交易輸入信息 if (this.isCoinbase()) { return; } // 再次驗證一下交易信息中的交易輸入是否正確,也就是可否查找對應的交易數據 for (TXInput txInput : this.getInputs()) { if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) { throw new Exception("ERROR: Previous transaction is not correct"); } } // 建立用於簽名的交易信息的副本 Transaction txCopy = this.trimmedCopy(); Security.addProvider(new BouncyCastleProvider()); Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); ecdsaSign.initSign(privateKey); for (int i = 0; i < txCopy.getInputs().length; i++) { TXInput txInputCopy = txCopy.getInputs()[i]; // 獲取交易輸入TxID對應的交易數據 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId())); // 獲取交易輸入所對應的上一筆交易中的交易輸出 TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()]; txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); txInputCopy.setSignature(null); // 獲得要簽名的數據,即交易ID txCopy.setTxId(txCopy.hash()); txInputCopy.setPubKey(null); // 對整個交易信息僅進行簽名,即對交易ID進行簽名 ecdsaSign.update(txCopy.getTxId()); byte[] signature = ecdsaSign.sign(); // 將整個交易數據的簽名賦值給交易輸入,由於交易輸入須要包含整個交易信息的簽名 // 注意是將獲得的簽名賦值給原交易信息中的交易輸入 this.getInputs()[i].setSignature(signature); } } ... }
這個方法須要私鑰和前面多筆交易集合做爲參數。正如前面所提到的那樣,爲了可以對交易信息進行簽名,咱們須要可以訪問到被交易數據中的交易輸入所引用的交易輸出,所以咱們須要獲得存儲這些交易輸出的交易信息。
讓咱們來一步一步review這個方法:
if (this.isCoinbase()) { return; }
因爲 coinbase 交易信息不存在交易輸入信息,所以它不須要簽名,直接return.
Transaction txCopy = this.trimmedCopy();
建立交易的副本
public class Transaction { ... /** * 建立用於簽名的交易數據副本 * * @return */ public Transaction trimmedCopy() { TXInput[] tmpTXInputs = new TXInput[this.getInputs().length]; for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i]; tmpTXInputs[i] = new TXInput(txInput.getTxId(), txInput.getTxOutputIndex(), null, null); } TXOutput[] tmpTXOutputs = new TXOutput[this.getOutputs().length]; for (int i = 0; i < this.getOutputs().length; i++) { TXOutput txOutput = this.getOutputs()[i]; tmpTXOutputs[i] = new TXOutput(txOutput.getValue(), txOutput.getPubKeyHash()); } return new Transaction(this.getTxId(), tmpTXInputs, tmpTXOutputs); } ... }
這個交易數據的副本包含了交易輸入與交易輸出,可是交易輸入的 Signature 與 PubKey 須要設置爲null。
使用私鑰初始化 SHA256withECDSA
簽名算法:
Security.addProvider(new BouncyCastleProvider()); Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); ecdsaSign.initSign(privateKey);
接下來,咱們迭代交易副本中的交易輸入:
for (TXInput txInput : txCopy.getInputs()) { // 獲取交易輸入TxID對應的交易數據 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId())); // 獲取交易輸入所對應的上一筆交易中的交易輸出 TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()]; txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); txInputCopy.setSignature(null);
在每個 txInput中,signature 都須要設置爲null
(僅僅是爲了二次確認檢查),而且 pubKey 設置爲它所引用的交易輸出的 pubKeyHash 字段。在此刻,除了當前的正在循環的交易輸入(txInput)外,其餘全部的交易輸入都是"空的",也就是說他們的 Signature
和 PubKey
字段被設置爲 null
。所以,交易輸入是被分開簽名的,儘管這對於咱們的應用並不十分緊要,可是比特幣容許交易包含引用了不一樣地址的輸入。
Hash
方法對交易進行序列化,並使用 SHA-256 算法進行哈希。哈希後的結果就是咱們要簽名的數據。在獲取完哈希,咱們應該重置 PubKey
字段,以便於它不會影響後面的迭代。
// 獲得要簽名的數據,即交易ID txCopy.setTxId(txCopy.hash()); txInput.setPubKey(null);
如今,最關鍵的部分來了:
// 對整個交易信息僅進行簽名,即對交易ID進行簽名 Security.addProvider(new BouncyCastleProvider()); Signature ecdsaSign = Signature.getInstance("SHA256withECDSA",BouncyCastleProvider.PROVIDER_NAME); ecdsaSign.initSign(privateKey); ecdsaSign.update(txCopy.getTxId()); byte[] signature = ecdsaSign.sign(); // 將整個交易數據的簽名賦值給交易輸入,由於交易輸入須要包含整個交易信息的簽名 // 注意是將獲得的簽名賦值給原交易信息中的交易輸入 this.getInputs()[i].setSignature(signature);
使用 SHA256withECDSA
簽名算法加上私鑰,來對交易ID進行簽名,從而獲得了交易輸入所要設置的交易簽名。
如今,讓咱們來實現交易的驗證功能:
public class Transaction { ... /** * 驗證交易信息 * * @param prevTxMap 前面多筆交易集合 * @return */ public boolean verify(Map<String, Transaction> prevTxMap) throws Exception { // coinbase 交易信息不須要簽名,也就無需驗證 if (this.isCoinbase()) { return true; } // 再次驗證一下交易信息中的交易輸入是否正確,也就是可否查找對應的交易數據 for (TXInput txInput : this.getInputs()) { if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) { throw new Exception("ERROR: Previous transaction is not correct"); } } // 建立用於簽名驗證的交易信息的副本 Transaction txCopy = this.trimmedCopy(); Security.addProvider(new BouncyCastleProvider()); ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1"); KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i]; // 獲取交易輸入TxID對應的交易數據 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId())); // 獲取交易輸入所對應的上一筆交易中的交易輸出 TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()]; TXInput txInputCopy = txCopy.getInputs()[i]; txInputCopy.setSignature(null); txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); // 獲得要簽名的數據,即交易ID txCopy.setTxId(txCopy.hash()); txInputCopy.setPubKey(null); // 使用橢圓曲線 x,y 點去生成公鑰Key BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33)); BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65)); ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y); ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters); PublicKey publicKey = keyFactory.generatePublic(keySpec); ecdsaVerify.initVerify(publicKey); ecdsaVerify.update(txCopy.getTxId()); if (!ecdsaVerify.verify(txInput.getSignature())) { return false; } } return true; } ... }
首選,同前面簽名同樣,咱們先獲取交易的拷貝數據:
Transaction txCopy = this.trimmedCopy();
獲取橢圓曲線參數和簽名類:
Security.addProvider(new BouncyCastleProvider()); ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1"); KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
接下來,咱們來檢查每個交易輸入的簽名是否正確:
for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i]; // 獲取交易輸入TxID對應的交易數據 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId())); // 獲取交易輸入所對應的上一筆交易中的交易輸出 TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()]; TXInput txInputCopy = txCopy.getInputs()[i]; txInputCopy.setSignature(null); txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); // 獲得要簽名的數據,即交易ID txCopy.setTxId(txCopy.hash()); txInputCopy.setPubKey(null); }
這部分與Sign方法中的相同,由於在驗證過程當中咱們須要簽署相同的數據。
// 使用橢圓曲線 x,y 點去生成公鑰Key BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33)); BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65)); ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y); ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters); PublicKey publicKey = keyFactory.generatePublic(keySpec); ecdsaVerify.initVerify(publicKey); ecdsaVerify.update(txCopy.getTxId()); if (!ecdsaVerify.verify(txInput.getSignature())) { return false; }
因爲交易輸入中存儲的 pubkey
,其實是橢圓曲線上的一對 x,y 座標,因此咱們能夠從 pubKey 獲得公鑰PublicKey
,而後在用公鑰去簽名進行驗證。若是驗證成功,則返回true,不然,返回false。
如今,咱們須要一個方法來獲取之前的交易。 因爲這須要與區塊鏈互動,咱們將使其成爲 blockchain 的一種方法:
public class Blockchain { ... /** * 依據交易ID查詢交易信息 * * @param txId 交易ID * @return */ private Transaction findTransaction(byte[] txId) throws Exception { for (BlockchainIterator iterator = this.getBlockchainIterator(); iterator.hashNext(); ) { Block block = iterator.next(); for (Transaction tx : block.getTransactions()) { if (Arrays.equals(tx.getTxId(), txId)) { return tx; } } } throw new Exception("ERROR: Can not found tx by txId ! "); } /** * 進行交易簽名 * * @param tx 交易數據 * @param privateKey 私鑰 */ public void signTransaction(Transaction tx, BCECPrivateKey privateKey) throws Exception { // 先來找到這筆新的交易中,交易輸入所引用的前面的多筆交易的數據 Map<String, Transaction> prevTxMap = new HashMap<>(); for (TXInput txInput : tx.getInputs()) { Transaction prevTx = this.findTransaction(txInput.getTxId()); prevTxMap.put(Hex.encodeHexString(txInput.getTxId()), prevTx); } tx.sign(privateKey, prevTxMap); } /** * 交易簽名驗證 * * @param tx */ private boolean verifyTransactions(Transaction tx) throws Exception { Map<String, Transaction> prevTx = new HashMap<>(); for (TXInput txInput : tx.getInputs()) { Transaction transaction = this.findTransaction(txInput.getTxId()); prevTx.put(Hex.encodeHexString(txInput.getTxId()), transaction); } return tx.verify(prevTx); } }
如今,咱們須要對咱們的交易進行真正的簽名和驗證了,交易的簽名發生在 newUTXOTransaction 中:
public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception { ... Transaction newTx = new Transaction(null, txInputs, txOutput); newTx.setTxId(newTx.hash()); // 進行交易簽名 blockchain.signTransaction(newTx, senderWallet.getPrivateKey()); return newTx; }
交易的驗證發生在一筆交易被放入區塊以前:
public void mineBlock(Transaction[] transactions) throws Exception { // 挖礦前,先驗證交易記錄 for (Transaction tx : transactions) { if (!this.verifyTransactions(tx)) { throw new Exception("ERROR: Fail to mine block ! Invalid transaction ! "); } } ... }
OK,讓咱們再一次對整個工程的代碼作一個測試,測試結果:
$ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 $ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB $ java -jar blockchain-java-jar-with-dependencies.jar createwallet wallet address : 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f $ java -jar blockchain-java-jar-with-dependencies.jar createblockchain -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 Elapsed Time: 164.961 seconds correct hash Hex: 00000524231ae1832c49957848d2d1871cc35ff4d113c23be1937c6dff5cdf2a Done ! $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 Balance of '1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6': 10 $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB -to 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f -amount 5 java.lang.Exception: ERROR: Not enough funds $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 -to 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB -amount 5 Elapsed Time: 54.92 seconds correct hash Hex: 00000354f86cde369d4c39d2b3016ac9a74956425f1348b4c26b2cddb98c100b Success! $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 Balance of '1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6': 5 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB Balance of '1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB': 5 $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f Balance of '13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f': 0
Good!沒有任何錯誤!
讓咱們註釋掉 NewUTXOTransaction
方法中的一行代碼,確保未被簽名的交易不能被添加到區塊中:
blockchain.signTransaction(newTx, senderWallet.getPrivateKey());
測試結果:
java.lang.Exception: Fail to verify transaction ! transaction invalid ! at one.wangwei.blockchain.block.Blockchain.verifyTransactions(Blockchain.java:334) at one.wangwei.blockchain.block.Blockchain.mineBlock(Blockchain.java:76) at one.wangwei.blockchain.cli.CLI.send(CLI.java:202) at one.wangwei.blockchain.cli.CLI.parse(CLI.java:79) at one.wangwei.blockchain.BlockchainTest.main(BlockchainTest.java:23)
這一節,咱們學到了:
到目前爲止,咱們已經實現了比特幣的許多關鍵特性! 咱們已經實現了除外網絡外的幾乎全部功能,而且在下一篇文章中,咱們將繼續完善交易這一環節機制。