基于Java语言构建区块链(五)—— 地址(钱包)
基于Java語言構建區塊鏈(五)—— 地址(錢包)
2018年03月25日 18:02:06?wangwei_hz?閱讀數:1292更多
個人分類:?區塊鏈bitcoin比特幣
文章的主要思想和內容均來自?https://jeiwan.cc/posts/building-blockchain-in-go-part-5/
原文鏈接:https://wangwei.one/posts/f9088e0f.html
引言
在?上一篇?文章當中,我們開始了交易機制的實現。你已經了解到交易的一些非個人特征:沒有用戶賬戶,您的個人數據(例如:姓名、護照號碼以及SSN(美國社會安全卡(Social Security Card)上的9 位數字))不是必需的,并且不存儲在比特幣的任何地方。但仍然必須有一些東西能夠識別你是這些交易輸出的所有者(例如:鎖定在這些輸出上的幣的所有者)。這就是比特幣地址的作用所在。到目前為止,我們只是使用了任意的用戶定義的字符串當做地址,現在是時候來實現真正的地址了,就像它們在比特幣中實現的一樣。
比特幣地址
這里有一個比特幣地址的示例:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。這是一個非常早期的比特幣地址,據稱是屬于中本聰的比特幣地址。比特幣地址是公開的。如果你想要給某人發送比特幣,你需要知道對方的比特幣地址。但是地址(盡管它是唯一的)并不能作為你是一個錢包所有者的憑證。事實上,這樣的地址是公鑰的一種可讀性更好的表示 。在比特幣中,你的身份是存儲在你計算機上(或存儲在你有權訪問的其他位置)的一對(或多對)私鑰和公鑰。比特幣依靠加密算法的組合來創建這些密鑰,并保證世界上沒有其他人任何人可以在沒有物理訪問密鑰的情況下訪問您的比特幣。
比特幣地址與公鑰不同。比特幣地址是由公鑰經過單向的哈希函數生成的
接下來,讓我們來討論一下這些加密算法。
注意:不要向本篇文章中的代碼所生成的任何比特幣地址發送真實的比特幣來進行測試,否則后果自負……
公鑰密碼學
公鑰加密算法(public-key cryptography)使用的是密鑰對:公鑰和私鑰。公鑰屬于非敏感信息,可以向任何人透露。相比之下,私鑰不能公開披露:除了所有者之外,任何人都不能擁有私鑰的權限,因為它是用作所有者標識的私鑰。你的私鑰代表就是你(當然是在加密貨幣世界里的)。
本質上,比特幣錢包就是一對這樣的密鑰。當你安裝一個錢包應用程序或者使用比特幣客戶端去生成一個新的地址時,它們就為你創建好了一個密鑰對。在比特幣種,誰控制了私鑰,誰就掌握了所有發往對應公鑰地址上所有比特幣的控制權。
私鑰和公鑰只是隨機的字節序列,因此它們不能被打印在屏幕上供人讀取。這就是為什么比特幣會用一種算法將公鑰的字節序列轉化為人類可讀的字符串形式。
如果你曾今使用過比特幣錢包的應用程序,它可能會為你生成助記詞密碼短語。這些助記詞可以用來替代私鑰,并且能夠生成私鑰。這種機制是通過?BIP-039?來實現的。
好了,現在我們已經知道在比特幣中由什么來決定用戶的標識了。但是,比特幣是如何校驗交易輸出(和它里面存儲的一些幣)的所有權的呢?
數字簽名
在數學和密碼學中,有個數字簽名的概念,這套算法保證了以下幾點:
通過對數據應用簽名算法(即簽署數據),可以得到一個簽名,以后可以對其進行驗證。數字簽名需要使用私鑰,而驗證則需要公鑰。
為了能夠簽署數據我們需要:
簽名操作會產生一個存儲在交易輸入中的簽名。為了能夠驗證一個簽名,我們需要:
簡單來講,這個驗證的過程可以被描述為:檢查簽名是由被簽名數據加上私鑰得來,并且這個公鑰也是由該私鑰生成。
數字簽名并不是一種加密方法,你無法從簽名反向構造出源數據。這個和我們?前面?提到過的Hash算法有點類似:通過對一個數據使用Hash算法,你可以得到該數據的唯一表示。它們兩者的不同之處在于,簽名算法多了一個密鑰對:它讓數字簽名得以驗證成為可能。
但是密鑰對也能夠用于去加密數據:私鑰用于加密數據,公鑰用于解密數據。不過比特幣并沒有使用加密算法。
在比特幣中,每一筆交易輸入都會被該筆交易的創建者進行簽名。比特幣中的每一筆交易在放入區塊之前都必須得到驗證。驗證的意思就是:
- 檢查交易輸入是否擁有引用前一筆交易中交易輸出的權限
- 檢查交易的簽名是否正確
數據簽名以及簽名驗證的過程如下圖所示:
讓我們來回顧一下交易的完整生命周期:
- 檢查交易輸入中公鑰的Hash值是否與它所引用的交易輸出的Hash值想匹配,這是確保發送方只能發送屬于他們自己的比特幣。
- 檢查簽名是否正確,這是為了確保這筆交易是由比特幣的真正所有者創建的。
橢圓曲線密碼學
正如前面所提到的那樣,公鑰和私鑰是一串隨機的字符序列。由于私鑰是用來識別比特幣所有者身份的緣故,因此有一個必要的條件:這個隨機算法必須產生真正的隨機序列。我們不希望意外地生成其他人所擁有的私鑰。也就是要保證隨機序列的絕對唯一性。
比特幣是使用的橢圓曲線來生成的私鑰。橢圓曲線是一個非常復雜的數學概念,這里我們不做詳細的介紹(如果你對此非常好奇,可以點擊?this gentle introduction to elliptic curves?進行詳細的 了解,警告:數學公式)。我們需要知道的是,這些曲線可以用來生成真正大而隨機的數字。比特幣所采用的曲線算法能夠隨機生成一個介于0到 2^2^56之間的數字(這是一個非常大的數字,用十進制表示的話,大約是10^77, 而整個可見的宇宙中,原子數在 10^78 到 10^82 之間) 。這么巨大的上限意味著產生兩個一樣的私鑰是幾乎不可能的事情。
另外,我們將會使用比特幣中所使用的 ECDSA (橢圓曲線數字簽名算法)去簽署交易信息。
Base58和Base58Check編碼
現在讓我們回到上面提到的比特幣地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?. 現在我們知道這個地址其實是公鑰的一種可讀高的表示方式。如果我們對他進行解碼,我們會看到公鑰看起來是這樣子的(字節序列的十六進制的表示方式):
0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93- 1
Base58
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
Base58Check是一種常用在比特幣中的Base58編碼格式,增加了錯誤校驗碼來檢查數據在轉錄中出現的錯誤。校驗碼長4個字節,添加到需要編碼的數據之后。校驗碼是從需要編碼的數據的哈希值中得到的,所以可以用來檢測并避免轉錄和輸入中產生的錯誤。使用Base58check編碼格式時,編碼軟件會計算原始數據的校驗碼并和結果數據中自帶的校驗碼進行對比。二者不匹配則表明有錯誤產生,那么這個Base58Check格式的數據就是無效的。例如,一個錯誤比特幣地址就不會被錢包認為是有效的地址,否則這種錯誤會造成資金的丟失。
為了使用Base58Check編碼格式對數據(數字)進行編碼,首先我們要對數據添加一個稱作“版本字節”的前綴,這個前綴用來明確需要編碼的數據的類型。例如,比特幣地址的前綴是0(十六進制是0x00),而對私鑰編碼時前綴是128(十六進制是0x80)。
讓我們以示意圖的形式展示一下從公鑰得到地址的過程:
因此,上述解碼的公鑰由三部分組成:
Version Public key hash Checksum 00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93- 1
- 2
由于哈希函數是單向的(也就說無法逆轉回去),所以不可能從一個哈希中提取公鑰。不過通過執行哈希函數并進行哈希比較,我們可以檢查一個公鑰是否被用于哈希的生成。
OK,現在我們有了所有的東西,讓我們來編寫一些代碼。 當一些概念被寫成代碼時,我們會對此理解的更加清晰和深刻。
地址實現
讓我們從?Wallet?的構成開始,這里我們需要先引入一個maven包:
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>1.59</version> </dependency>- 1
- 2
- 3
- 4
- 5
錢包結構
/*** 錢包** @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 ProviderSecurity.addProvider(new BouncyCastleProvider());// 創建橢圓曲線算法的密鑰對生成器,算法為 ECDSAKeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);// 橢圓曲線(EC)域參數設定// bitcoin 為什么會選擇 secp256k1,詳見:https://bitcointalk.org/index.php?topic=151120.0ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");g.initialize(ecSpec, new SecureRandom());return g.generateKeyPair();}}- 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
所謂的錢包,其實本質上就是一個密鑰對。這里我們需要借助?KeyPairGenerator?生成密鑰對。
接著,我們來生成比特幣的錢包地址:
public class Wallet {.../*** 獲取錢包地址** @return*/public String getAddress() throws Exception {// 1. 獲取 ripemdHashedKeybyte[] ripemdHashedKey = BtcAddressUtils.ripeMD160Hash(this.getPublicKey().getEncoded());// 2. 添加版本 0x00ByteArrayOutputStream 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);}... }- 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
這個時候,你就可以得到?真實的比特幣地址?了,并且你可以到?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);}}- 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
交易輸出
/*** 交易輸出** @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);}}- 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
代碼中還有很多其他的地方需要變動,這里不一一指出,詳見文末的源碼連接。
注意,由于我們不會去實現腳本語言特性,所以我們不再使用?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 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdhElapsed 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- 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
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);// 得到要簽名的數據,即交易IDtxCopy.setTxId(txCopy.hash());txInputCopy.setPubKey(null);// 對整個交易信息僅進行簽名,即對交易ID進行簽名ecdsaSign.update(txCopy.getTxId());byte[] signature = ecdsaSign.sign();// 將整個交易數據的簽名賦值給交易輸入,因為交易輸入需要包含整個交易信息的簽名// 注意是將得到的簽名賦值給原交易信息中的交易輸入this.getInputs()[i].setSignature(signature);}}...}- 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
這個方法需要私鑰和前面多筆交易集合作為參數。正如前面所提到的那樣,為了能夠對交易信息進行簽名,我們需要能夠訪問到被交易數據中的交易輸入所引用的交易輸出,因此我們需要得到存儲這些交易輸出的交易信息。
讓我們來一步一步review這個方法:
if (this.isCoinbase()) {return; }- 1
- 2
- 3
由于 coinbase 交易信息不存在交易輸入信息,因此它不需要簽名,直接return.
Transaction txCopy = this.trimmedCopy();- 1
創建交易的副本
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);}...}- 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
這個交易數據的副本包含了交易輸入與交易輸出,但是交易輸入的?Signature?與?PubKey?需要設置為null。
使用私鑰初始化?SHA256withECDSA?簽名算法:
Security.addProvider(new BouncyCastleProvider());Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);ecdsaSign.initSign(privateKey);- 1
- 2
- 3
接下來,我們迭代交易副本中的交易輸入:
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);- 1
- 2
- 3
- 4
- 5
- 6
- 7
在每一個 txInput中,signature?都需要設置為null(僅僅是為了二次確認檢查),并且?pubKey?設置為它所引用的交易輸出的?pubKeyHash?字段。在此刻,除了當前的正在循環的交易輸入(txInput)外,其他所有的交易輸入都是”空的”,也就是說他們的?Signature?和?PubKey?字段被設置為?null。因此,交易輸入是被分開簽名的,盡管這對于我們的應用并不十分緊要,但是比特幣允許交易包含引用了不同地址的輸入。
Hash?方法對交易進行序列化,并使用 SHA-256 算法進行哈希。哈希后的結果就是我們要簽名的數據。在獲取完哈希,我們應該重置?PubKey?字段,以便于它不會影響后面的迭代。
// 得到要簽名的數據,即交易ID txCopy.setTxId(txCopy.hash()); txInput.setPubKey(null);- 1
- 2
- 3
現在,最關鍵的部分來了:
// 對整個交易信息僅進行簽名,即對交易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);- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
使用?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());// 得到要簽名的數據,即交易IDtxCopy.setTxId(txCopy.hash());txInputCopy.setPubKey(null);// 使用橢圓曲線 x,y 點去生成公鑰KeyBigInteger 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;}... }- 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
首選,同前面簽名一樣,我們先獲取交易的拷貝數據:
Transaction txCopy = this.trimmedCopy();- 1
獲取橢圓曲線參數和簽名類:
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);- 1
- 2
- 3
- 4
接下來,我們來檢查每一個交易輸入的簽名是否正確:
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());// 得到要簽名的數據,即交易IDtxCopy.setTxId(txCopy.hash());txInputCopy.setPubKey(null); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
這部分與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; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
由于交易輸入中存儲的?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);}}- 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
現在,我們需要對我們的交易進行真正的簽名和驗證了,交易的簽名發生在?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; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
交易的驗證發生在一筆交易被放入區塊之前:
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 ! ");}}... }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
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 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6Elapsed 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- 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
Good!沒有任何錯誤!
讓我們注釋掉?NewUTXOTransaction?方法中的一行代碼,確保未被簽名的交易不能被添加到區塊中:
blockchain.signTransaction(newTx, senderWallet.getPrivateKey());- 1
測試結果:
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)- 1
- 2
- 3
- 4
- 5
- 6
總結
這一節,我們學到了:
到目前為止,我們已經實現了比特幣的許多關鍵特性! 我們已經實現了除外網絡外的幾乎所有功能,并且在下一篇文章中,我們將繼續完善交易這一環節機制。
資料
-
源代碼:https://github.com/wangweiX/blockchain-java/tree/part5-wallet
-
Elliptic Curve Key Pair Generation and Key Factories
-
How to create public key objects with x and y coordinates?
-
Public-key cryptography
-
Digital signatures
-
Elliptic curve
-
Elliptic curve cryptography
-
ECDSA
-
Technical background of Bitcoin addresses
-
Address
-
Base58
-
A gentle introduction to elliptic curve cryptography
?
總結
以上是生活随笔為你收集整理的基于Java语言构建区块链(五)—— 地址(钱包)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于Java语言构建区块链(六)—— 交
- 下一篇: 基于Java语言构建区块链(四)—— 交