数字签名(digital signature)

论坛 期权论坛 期权     
枭一   2019-10-27 05:53   2493   0
在数学和密码学中,有一个数字签名(digital signature)的概念,算法可以保证:
  • 当数据从发送方传送到接收方时,数据不会被修改;
  • 数据由某一确定的发送方创建;
  • 发送方无法否认发送过数据这一事实。
通过在数据上应用签名算法(也就是对数据进行签名),你就可以得到一个签名,这个签名晚些时候会被验证。生成数字签名需要一个私钥,而验证签名需要一个公钥。签名有点类似于印章,比方说我做了一幅画,完了用印章一盖,就说明了这幅画是我的作品。给数据生成签名,就是给数据盖了章。
[h1]1. 课程目标[/h1]
  • 知道什么是数字签名
  • 知道为什么要进行签名和验签
  • 学会如何进行签名
  • 学会在何处使用签名
[h1]2. 项目代码及效果展示[/h1][h2]2.1 项目代码结构[/h2]

[h2]2.2 项目运行结果[/h2][iframe]https://mp.weixin.qq.com/mp/readtemplate?t=pages/video_player_tmpl&action=mpvideo&auto=0&vid=wxv_847121304712101888[/iframe]
其实运行效果和之前的没有区别,因为我们并没有增加新的功能,只是在创建交易的时候,添加了数字签名,在创建新区块的时候,进行签名验证。
[h1]3. 创建项目[/h1][h2]3.1 创建工程[/h2]打开IntelliJ IDEA的工作空间,将上一个项目代码目录
  1. part6_Wallet
复制代码
,复制为
  1. part7_Signature
复制代码

然后打开IntelliJ IDEA开发工具。
打开工程:
  1. part7_Signature
复制代码
,并删除target目录。然后进行以下修改:
  1. step1:先将项目重新命名为:part7_Signature。
  2. step2:修改pom.xml配置文件。
  3. 改为:part7_Signature标签
  4. 改为:part7_Signature Maven Webapp
  5. 说明:我们每一章节的项目代码,都是在上一个章节上进行添加。
  6. 所以拷贝上一次的项目代码,然后进行新内容的添加或修改。
复制代码
[h2]3.2 代码实现[/h2][h3]3.2.1 修改
  1. TXOutput.java
复制代码
文件[/h3]打开
  1. cldy.hanru.blockchain.transaction
复制代码
包,修改
  1. TXOutput.java
复制代码
文件。
修改步骤:
  1. 修改步骤:
  2. step1:修改字段pubKeyHash
  3. step2:添加方法isLockedWithKey()
  4. step3:添加方法newTXOutput()
复制代码
修改完后代码如下:
  1. package cldy.hanru.blockchain.transaction;
  2. import cldy.hanru.blockchain.util.Base58Check;
  3. import lombok.AllArgsConstructor;
  4. import lombok.Data;
  5. import lombok.NoArgsConstructor;
  6. import java.util.Arrays;
  7. /**
  8. * @author hanru
  9. */
  10. @Data
  11. @AllArgsConstructor
  12. @NoArgsConstructor
  13. public class TXOutput {
  14. /**
  15. * 数值金额
  16. */
  17. private int value;
  18. /**
  19. * 锁定脚本
  20. */
  21. // private String scriptPubKey;
  22. /**
  23. * 公钥Hash
  24. */
  25. private byte[] pubKeyHash;
  26. /**
  27. * 判断解锁数据是否能够解锁交易输出
  28. *
  29. * @param unlockingData
  30. * @return
  31. */
  32. // public boolean canBeUnlockedWith(String unlockingData) {
  33. //
  34. // return this.getScriptPubKey().endsWith(unlockingData);
  35. // }
  36. /**
  37. * 检查交易输出是否能够使用指定的公钥
  38. *
  39. * @param pubKeyHash
  40. * @return
  41. */
  42. public boolean isLockedWithKey(byte[] pubKeyHash) {
  43. return Arrays.equals(this.getPubKeyHash(), pubKeyHash);
  44. }
  45. /**
  46. * 创建交易输出
  47. *
  48. * @param value
  49. * @param address
  50. * @return
  51. */
  52. public static TXOutput newTXOutput(int value, String address) {
  53. // 反向转化为 byte 数组
  54. byte[] versionedPayload = Base58Check.base58ToBytes(address);
  55. byte[] pubKeyHash = Arrays.copyOfRange(versionedPayload, 1, versionedPayload.length);
  56. return new TXOutput(value, pubKeyHash);
  57. }
  58. }
复制代码
[h3]3.2.2 修改
  1. TXInput.java
复制代码
文件[/h3]打开
  1. cldy.hanru.blockchain.transaction
复制代码
包,修改
  1. TXInput.java
复制代码
文件。
修改步骤:
  1. 修改步骤:
  2. step1:修改字段signature和pubKey
  3. step2:添加方法usesKey()
复制代码
修改完后代码如下:
  1. package cldy.hanru.blockchain.transaction;
  2. import cldy.hanru.blockchain.util.AddressUtils;
  3. import lombok.AllArgsConstructor;
  4. import lombok.Data;
  5. import lombok.NoArgsConstructor;
  6. import java.util.Arrays;
  7. /**
  8. * @author hanru
  9. */
  10. @AllArgsConstructor
  11. @NoArgsConstructor
  12. @Data
  13. public class TXInput {
  14. /**
  15. * 交易Id的hash值
  16. */
  17. private byte[] txId;
  18. /**
  19. * 交易输出索引
  20. */
  21. private int txOutputIndex;
  22. /**
  23. * 解锁脚本
  24. */
  25. // private String scriptSig;
  26. /**
  27. * 签名
  28. */
  29. private byte[] signature;
  30. /**
  31. * 公钥
  32. */
  33. private byte[] pubKey;
  34. /**
  35. * 判断解锁数据是否能够解锁交易输出
  36. *
  37. * @param unlockingData
  38. * @return
  39. */
  40. // public boolean canUnlockOutputWith(String unlockingData) {
  41. // return this.getScriptSig().endsWith(unlockingData);
  42. // }
  43. /**
  44. * 检查公钥hash是否用于交易输入
  45. *
  46. * @param pubKeyHash
  47. * @return
  48. */
  49. public boolean usesKey(byte[] pubKeyHash) {
  50. byte[] lockingHash = AddressUtils.ripeMD160Hash(this.getPubKey());
  51. return Arrays.equals(lockingHash, pubKeyHash);
  52. }
  53. }
复制代码
[h3]3.2.3 修改
  1. Transaction.java
复制代码
文件[/h3]打开
  1. cldy.hanru.blockchain.transaction
复制代码
包,修改
  1. Transaction.java
复制代码
文件。
修改步骤:
  1. 修改步骤:
  2. step1:修改newCoinbaseTX()方法
  3. step2:添加getData()
  4. step3:添加TrimmedCopy()方法,用于备份签名和验证时交易的副本。
  5. step4:添加Sign()方法,用于进行交易签名
  6. step5:添加Verify()方法,用于签名验证
复制代码
修改完后代码如下:
  1. package cldy.hanru.blockchain.transaction;
  2. import cldy.hanru.blockchain.block.Blockchain;
  3. import cldy.hanru.blockchain.util.AddressUtils;
  4. import cldy.hanru.blockchain.util.SerializeUtils;
  5. import cldy.hanru.blockchain.wallet.Wallet;
  6. import cldy.hanru.blockchain.wallet.WalletUtils;
  7. import lombok.AllArgsConstructor;
  8. import lombok.Data;
  9. import lombok.NoArgsConstructor;
  10. import org.apache.commons.codec.binary.Hex;
  11. import org.apache.commons.codec.digest.DigestUtils;
  12. import org.apache.commons.lang3.ArrayUtils;
  13. import org.apache.commons.lang3.StringUtils;
  14. import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
  15. import org.bouncycastle.jce.ECNamedCurveTable;
  16. import org.bouncycastle.jce.provider.BouncyCastleProvider;
  17. import org.bouncycastle.jce.spec.ECParameterSpec;
  18. import org.bouncycastle.jce.spec.ECPublicKeySpec;
  19. import org.bouncycastle.math.ec.ECPoint;
  20. import java.math.BigInteger;
  21. import java.security.KeyFactory;
  22. import java.security.PublicKey;
  23. import java.security.Security;
  24. import java.security.Signature;
  25. import java.util.Arrays;
  26. import java.util.Iterator;
  27. import java.util.Map;
  28. /**
  29. * @author hanru
  30. */
  31. @Data
  32. @AllArgsConstructor
  33. @NoArgsConstructor
  34. public class Transaction {
  35. private static final int SUBSIDY = 10;
  36. /**
  37. * 交易的Hash
  38. */
  39. private byte[] txId;
  40. /**
  41. * 交易输入
  42. */
  43. private TXInput[] inputs;
  44. /**
  45. * 交易输出
  46. */
  47. private TXOutput[] outputs;
  48. /**
  49. * 设置交易ID
  50. */
  51. private void setTxId() {
  52. this.setTxId(DigestUtils.sha256(SerializeUtils.serialize(this)));
  53. }
  54. /**
  55. * 要签名的数据
  56. * @return
  57. */
  58. public byte[] getData() {
  59. // 使用序列化的方式对Transaction对象进行深度复制
  60. byte[] serializeBytes = SerializeUtils.serialize(this);
  61. Transaction copyTx = (Transaction) SerializeUtils.deserialize(serializeBytes);
  62. copyTx.setTxId(new byte[]{});
  63. return DigestUtils.sha256(SerializeUtils.serialize(copyTx));
  64. }
  65. /**
  66. * 创建CoinBase交易
  67. *
  68. * @param to 收账的钱包地址
  69. * @param data 解锁脚本数据
  70. * @return
  71. */
  72. public static Transaction newCoinbaseTX(String to, String data) {
  73. if (StringUtils.isBlank(data)) {
  74. data = String.format("Reward to '%s'", to);
  75. }
  76. // 创建交易输入
  77. // TXInput txInput = new TXInput(new byte[]{}, -1, data);
  78. TXInput txInput = new TXInput(new byte[]{}, -1, null, data.getBytes());
  79. // 创建交易输出
  80. // TXOutput txOutput = new TXOutput(SUBSIDY, to);
  81. TXOutput txOutput = TXOutput.newTXOutput(SUBSIDY, to);
  82. // 创建交易
  83. Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput});
  84. // 设置交易ID
  85. tx.setTxId();
  86. // tx.setTxId(tx.hash());
  87. return tx;
  88. }
  89. /**
  90. * 从 from 向 to 支付一定的 amount 的金额
  91. *
  92. * @param from 支付钱包地址
  93. * @param to 收款钱包地址
  94. * @param amount 交易金额
  95. * @param blockchain 区块链
  96. * @return
  97. */
  98. public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception {
  99. // 获取钱包
  100. Wallet senderWallet = WalletUtils.getInstance().getWallet(from);
  101. byte[] pubKey = senderWallet.getPublicKey();
  102. byte[] pubKeyHash = AddressUtils.ripeMD160Hash(pubKey);
  103. // SpendableOutputResult result = blockchain.findSpendableOutputs(from, amount);
  104. SpendableOutputResult result = blockchain.findSpendableOutputs(pubKeyHash, amount);
  105. int accumulated = result.getAccumulated();
  106. Map unspentOuts = result.getUnspentOuts();
  107. if (accumulated < amount) {
  108. throw new Exception("ERROR: Not enough funds");
  109. }
  110. Iterator iterator = unspentOuts.entrySet().iterator();
  111. TXInput[] txInputs = {};
  112. while (iterator.hasNext()) {
  113. Map.Entry entry = iterator.next();
  114. String txIdStr = entry.getKey();
  115. int[] outIdxs = entry.getValue();
  116. byte[] txId = Hex.decodeHex(txIdStr);
  117. for (int outIndex : outIdxs) {
  118. // txInputs = ArrayUtils.add(txInputs, new TXInput(txId, outIndex, from));
  119. txInputs = ArrayUtils.add(txInputs, new TXInput(txId, outIndex, null, pubKey));
  120. }
  121. }
  122. TXOutput[] txOutput = {};
  123. // txOutput = ArrayUtils.add(txOutput, new TXOutput(amount, to));
  124. txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput(amount, to));
  125. if (accumulated > amount) {
  126. // txOutput = ArrayUtils.add(txOutput, new TXOutput((accumulated - amount), from));
  127. txOutput = ArrayUtils.add(txOutput, TXOutput.newTXOutput((accumulated - amount), from));
  128. }
  129. Transaction newTx = new Transaction(null, txInputs, txOutput);
  130. newTx.setTxId();
  131. // newTx.setTxId(newTx.hash());
  132. // 进行交易签名
  133. blockchain.signTransaction(newTx, senderWallet.getPrivateKey());
  134. return newTx;
  135. }
  136. /**
  137. * 是否为 Coinbase 交易
  138. *
  139. * @return
  140. */
  141. public boolean isCoinbase() {
  142. return this.getInputs().length == 1
  143. && this.getInputs()[0].getTxId().length == 0
  144. && this.getInputs()[0].getTxOutputIndex() == -1;
  145. }
  146. /**
  147. * 创建用于签名的交易数据副本,交易输入的 signature 和 pubKey 需要设置为null
  148. *
  149. * @return
  150. */
  151. public Transaction trimmedCopy() {
  152. TXInput[] tmpTXInputs = new TXInput[this.getInputs().length];
  153. for (int i = 0; i < this.getInputs().length; i++) {
  154. TXInput txInput = this.getInputs()[i];
  155. tmpTXInputs[i] = new TXInput(txInput.getTxId(), txInput.getTxOutputIndex(), null, null);
  156. }
  157. TXOutput[] tmpTXOutputs = new TXOutput[this.getOutputs().length];
  158. for (int i = 0; i < this.getOutputs().length; i++) {
  159. TXOutput txOutput = this.getOutputs()[i];
  160. tmpTXOutputs[i] = new TXOutput(txOutput.getValue(), txOutput.getPubKeyHash());
  161. }
  162. return new Transaction(this.getTxId(), tmpTXInputs, tmpTXOutputs);
  163. }
  164. /**
  165. * 签名
  166. *
  167. * @param privateKey 私钥
  168. * @param prevTxMap 前面多笔交易集合
  169. */
  170. public void sign(BCECPrivateKey privateKey, Map prevTxMap) throws Exception {
  171. // coinbase 交易信息不需要签名,因为它不存在交易输入信息
  172. if (this.isCoinbase()) {
  173. return;
  174. }
  175. // 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据
  176. for (TXInput txInput : this.getInputs()) {
  177. if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) {
  178. throw new Exception("ERROR: Previous transaction is not correct");
  179. }
  180. }
  181. // 创建用于签名的交易信息的副本
  182. Transaction txCopy = this.trimmedCopy();
  183. Security.addProvider(new BouncyCastleProvider());
  184. Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
  185. ecdsaSign.initSign(privateKey);
  186. for (int i = 0; i < txCopy.getInputs().length; i++) {
  187. TXInput txInputCopy = txCopy.getInputs()[i];
  188. // 获取交易输入TxID对应的交易数据
  189. Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId()));
  190. // 获取交易输入所对应的上一笔交易中的交易输出
  191. TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()];
  192. txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
  193. txInputCopy.setSignature(null);
  194. // 得到要签名的数据
  195. byte[] signData = txCopy.getData();
  196. txInputCopy.setPubKey(null);
  197. // 对整个交易信息仅进行签名
  198. ecdsaSign.update(signData);
  199. byte[] signature = ecdsaSign.sign();
  200. // 将整个交易数据的签名赋值给交易输入,因为交易输入需要包含整个交易信息的签名
  201. // 注意是将得到的签名赋值给原交易信息中的交易输入
  202. this.getInputs()[i].setSignature(signature);
  203. }
  204. }
  205. /**
  206. * 验证交易信息
  207. *
  208. * @param prevTxMap 前面多笔交易集合
  209. * @return
  210. */
  211. public boolean verify(Map prevTxMap) throws Exception {
  212. // coinbase 交易信息不需要签名,也就无需验证
  213. if (this.isCoinbase()) {
  214. return true;
  215. }
  216. // 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据
  217. for (TXInput txInput : this.getInputs()) {
  218. if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) {
  219. throw new Exception("ERROR: Previous transaction is not correct");
  220. }
  221. }
  222. // 创建用于签名验证的交易信息的副本
  223. Transaction txCopy = this.trimmedCopy();
  224. Security.addProvider(new BouncyCastleProvider());
  225. ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1");
  226. KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);
  227. Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
  228. for (int i = 0; i < this.getInputs().length; i++) {
  229. TXInput txInput = this.getInputs()[i];
  230. // 获取交易输入TxID对应的交易数据
  231. Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId()));
  232. // 获取交易输入所对应的上一笔交易中的交易输出
  233. TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()];
  234. TXInput txInputCopy = txCopy.getInputs()[i];
  235. txInputCopy.setSignature(null);
  236. txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
  237. // 得到要签名的数据
  238. byte[] signData = txCopy.getData();
  239. txInputCopy.setPubKey(null);
  240. // 使用椭圆曲线 x,y 点去生成公钥Key
  241. BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33));
  242. BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65));
  243. ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y);
  244. ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters);
  245. PublicKey publicKey = keyFactory.generatePublic(keySpec);
  246. ecdsaVerify.initVerify(publicKey);
  247. ecdsaVerify.update(signData);
  248. if (!ecdsaVerify.verify(txInput.getSignature())) {
  249. return false;
  250. }
  251. }
  252. return true;
  253. }
  254. }
复制代码
[h3]3.2.4 修改
  1. BlockChain.java
复制代码
文件[/h3]打开
  1. cldy.hanru.blockchain.block
复制代码
包,修改
  1. BlockChain.java
复制代码
文件。
修改步骤:
  1. 修改步骤:
  2. step1:修改getAllSpentTXOs()
  3. step2:修改findUTXO()
  4. step3:修改findSpendableOutputs()
  5. step4:添加方法findTransaction()
  6. step5:添加signTransaction()
  7. step6:添加verifyTransactions()
  8. step7:修改mineBlock(),添加验证
复制代码
修改完后代码如下:
  1. package cldy.hanru.blockchain.block;
  2. import cldy.hanru.blockchain.store.RocksDBUtils;
  3. import cldy.hanru.blockchain.transaction.SpendableOutputResult;
  4. import cldy.hanru.blockchain.transaction.TXInput;
  5. import cldy.hanru.blockchain.transaction.TXOutput;
  6. import cldy.hanru.blockchain.transaction.Transaction;
  7. import cldy.hanru.blockchain.util.ByteUtils;
  8. import lombok.AllArgsConstructor;
  9. import lombok.Data;
  10. import org.apache.commons.codec.binary.Hex;
  11. import org.apache.commons.lang3.ArrayUtils;
  12. import org.apache.commons.lang3.StringUtils;
  13. import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
  14. import java.util.Arrays;
  15. import java.util.HashMap;
  16. import java.util.List;
  17. import java.util.Map;
  18. /**
  19. * 区块链
  20. *
  21. * @author hanru
  22. */
  23. @Data
  24. @AllArgsConstructor
  25. public class Blockchain {
  26. /**
  27. * 最后一个区块的hash
  28. */
  29. private String lastBlockHash;
  30. /**
  31. * 创建区块链,createBlockchain
  32. *
  33. * @param address
  34. * @return
  35. */
  36. public static Blockchain createBlockchain(String address) {
  37. String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();
  38. if (StringUtils.isBlank(lastBlockHash)) {
  39. //对应的bucket不存在,说明是第一次获取区块链实例
  40. // 创建 coinBase 交易
  41. Transaction coinbaseTX = Transaction.newCoinbaseTX(address, "");
  42. Block genesisBlock = Block.newGenesisBlock(coinbaseTX);
  43. // Block genesisBlock = Block.newGenesisBlock();
  44. lastBlockHash = genesisBlock.getHash();
  45. RocksDBUtils.getInstance().putBlock(genesisBlock);
  46. RocksDBUtils.getInstance().putLastBlockHash(lastBlockHash);
  47. }
  48. return new Blockchain(lastBlockHash);
  49. }
  50. /**
  51. * 根据block,添加区块
  52. *
  53. * @param block
  54. */
  55. public void addBlock(Block block) {
  56. RocksDBUtils.getInstance().putLastBlockHash(block.getHash());
  57. RocksDBUtils.getInstance().putBlock(block);
  58. this.lastBlockHash = block.getHash();
  59. }
  60. /**
  61. * 根据data添加区块
  62. * @param data
  63. */
  64. // public void addBlock(String data) throws Exception{
  65. //
  66. // String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();
  67. // if (StringUtils.isBlank(lastBlockHash)){
  68. // throw new Exception("还没有数据库,无法直接添加区块。。");
  69. // }
  70. // this.addBlock(Block.newBlock(lastBlockHash,data));
  71. // }
  72. /**
  73. * 区块链迭代器:内部类
  74. */
  75. public class BlockchainIterator {
  76. /**
  77. * 当前区块的hash
  78. */
  79. private String currentBlockHash;
  80. /**
  81. * 构造函数
  82. *
  83. * @param currentBlockHash
  84. */
  85. public BlockchainIterator(String currentBlockHash) {
  86. this.currentBlockHash = currentBlockHash;
  87. }
  88. /**
  89. * 判断是否有下一个区块
  90. *
  91. * @return
  92. */
  93. public boolean hashNext() {
  94. if (ByteUtils.ZERO_HASH.equals(currentBlockHash)) {
  95. return false;
  96. }
  97. Block lastBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash);
  98. if (lastBlock == null) {
  99. return false;
  100. }
  101. // 如果是创世区块
  102. if (ByteUtils.ZERO_HASH.equals(lastBlock.getPrevBlockHash())) {
  103. return true;
  104. }
  105. return RocksDBUtils.getInstance().getBlock(lastBlock.getPrevBlockHash()) != null;
  106. }
  107. /**
  108. * 迭代获取区块
  109. *
  110. * @return
  111. */
  112. public Block next() {
  113. Block currentBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash);
  114. if (currentBlock != null) {
  115. this.currentBlockHash = currentBlock.getPrevBlockHash();
  116. return currentBlock;
  117. }
  118. return null;
  119. }
  120. }
  121. /**
  122. * 添加方法,用于获取迭代器实例
  123. *
  124. * @return
  125. */
  126. public BlockchainIterator getBlockchainIterator() {
  127. return new BlockchainIterator(lastBlockHash);
  128. }
  129. /**
  130. * 打包交易,进行挖矿
  131. *
  132. * @param transactions
  133. */
  134. public void mineBlock(List transactions) throws Exception {
  135. // 挖矿前,先验证交易记录
  136. for (Transaction tx : transactions) {
  137. if (!this.verifyTransactions(tx)) {
  138. throw new Exception("ERROR: Fail to mine block ! Invalid transaction ! ");
  139. }
  140. }
  141. String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();
  142. Block lastBlock = RocksDBUtils.getInstance().getBlock(lastBlockHash);
  143. if (lastBlockHash == null) {
  144. throw new Exception("ERROR: Fail to get last block hash ! ");
  145. }
  146. Block block = Block.newBlock(lastBlockHash, transactions,lastBlock.getHeight()+1);
  147. this.addBlock(block);
  148. }
  149. /**
  150. * 从交易输入中查询区块链中所有已被花费了的交易输出
  151. *
  152. * @param pubKeyHash 钱包公钥Hash
  153. * @return 交易ID以及对应的交易输出下标地址
  154. * @throws Exception
  155. */
  156. private Map getAllSpentTXOs(byte[] pubKeyHash) {
  157. // 定义TxId ——> spentOutIndex[],存储交易ID与已被花费的交易输出数组索引值
  158. Map spentTXOs = new HashMap();
  159. for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) {
  160. Block block = blockchainIterator.next();
  161. for (Transaction transaction : block.getTransactions()) {
  162. // 如果是 coinbase 交易,直接跳过,因为它不存在引用前一个区块的交易输出
  163. if (transaction.isCoinbase()) {
  164. continue;
  165. }
  166. for (TXInput txInput : transaction.getInputs()) {
  167. // if (txInput.canUnlockOutputWith(address)) {
  168. if (txInput.usesKey(pubKeyHash)) {
  169. String inTxId = Hex.encodeHexString(txInput.getTxId());
  170. int[] spentOutIndexArray = spentTXOs.get(inTxId);
  171. if (spentOutIndexArray == null) {
  172. spentTXOs.put(inTxId, new int[]{txInput.getTxOutputIndex()});
  173. } else {
  174. spentOutIndexArray = ArrayUtils.add(spentOutIndexArray, txInput.getTxOutputIndex());
  175. spentTXOs.put(inTxId, spentOutIndexArray);
  176. }
  177. }
  178. }
  179. }
  180. }
  181. return spentTXOs;
  182. }
  183. /**
  184. * 查找钱包地址对应的所有未花费的交易
  185. *
  186. * @param pubKeyHash 钱包公钥Hash
  187. * @return
  188. */
  189. private Transaction[] findUnspentTransactions(byte[] pubKeyHash) throws Exception {
  190. Map allSpentTXOs = this.getAllSpentTXOs(pubKeyHash);
  191. Transaction[] unspentTxs = {};
  192. // 再次遍历所有区块中的交易输出
  193. for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) {
  194. Block block = blockchainIterator.next();
  195. for (Transaction transaction : block.getTransactions()) {
  196. String txId = Hex.encodeHexString(transaction.getTxId());
  197. int[] spentOutIndexArray = allSpentTXOs.get(txId);
  198. for (int outIndex = 0; outIndex < transaction.getOutputs().length; outIndex++) {
  199. if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) {
  200. continue;
  201. }
  202. // 保存不存在 allSpentTXOs 中的交易
  203. // if (transaction.getOutputs()[outIndex].canBeUnlockedWith(address)) {
  204. if (transaction.getOutputs()[outIndex].isLockedWithKey(pubKeyHash)) {
  205. unspentTxs = ArrayUtils.add(unspentTxs, transaction);
  206. }
  207. }
  208. }
  209. }
  210. return unspentTxs;
  211. }
  212. /**
  213. * 查找钱包地址对应的所有UTXO
  214. *
  215. * @param pubKeyHash 钱包公钥Hash
  216. * @return
  217. */
  218. public TXOutput[] findUTXO(byte[] pubKeyHash) throws Exception {
  219. Transaction[] unspentTxs = this.findUnspentTransactions(pubKeyHash);
  220. TXOutput[] utxos = {};
  221. if (unspentTxs == null || unspentTxs.length == 0) {
  222. return utxos;
  223. }
  224. for (Transaction tx : unspentTxs) {
  225. for (TXOutput txOutput : tx.getOutputs()) {
  226. // if (txOutput.canBeUnlockedWith(address)) {
  227. if (txOutput.isLockedWithKey(pubKeyHash)) {
  228. utxos = ArrayUtils.add(utxos, txOutput);
  229. }
  230. }
  231. }
  232. return utxos;
  233. }
  234. /**
  235. * 寻找能够花费的交易
  236. *
  237. * @param pubKeyHash 钱包公钥Hash
  238. * @param amount 花费金额
  239. */
  240. public SpendableOutputResult findSpendableOutputs(byte[] pubKeyHash, int amount) throws Exception {
  241. Transaction[] unspentTXs = this.findUnspentTransactions(pubKeyHash);
  242. int accumulated = 0;
  243. Map unspentOuts = new HashMap();
  244. for (Transaction tx : unspentTXs) {
  245. String txId = Hex.encodeHexString(tx.getTxId());
  246. for (int outId = 0; outId < tx.getOutputs().length; outId++) {
  247. TXOutput txOutput = tx.getOutputs()[outId];
  248. // if (txOutput.canBeUnlockedWith(address) && accumulated < amount) {
  249. if (txOutput.isLockedWithKey(pubKeyHash) && accumulated < amount) {
  250. accumulated += txOutput.getValue();
  251. int[] outIds = unspentOuts.get(txId);
  252. if (outIds == null) {
  253. outIds = new int[]{outId};
  254. } else {
  255. outIds = ArrayUtils.add(outIds, outId);
  256. }
  257. unspentOuts.put(txId, outIds);
  258. if (accumulated >= amount) {
  259. break;
  260. }
  261. }
  262. }
  263. }
  264. return new SpendableOutputResult(accumulated, unspentOuts);
  265. }
  266. /**
  267. * 依据交易ID查询交易信息
  268. *
  269. * @param txId 交易ID
  270. * @return
  271. */
  272. private Transaction findTransaction(byte[] txId) throws Exception {
  273. for (BlockchainIterator iterator = this.getBlockchainIterator(); iterator.hashNext(); ) {
  274. Block block = iterator.next();
  275. for (Transaction tx : block.getTransactions()) {
  276. if (Arrays.equals(tx.getTxId(), txId)) {
  277. return tx;
  278. }
  279. }
  280. }
  281. throw new Exception("ERROR: Can not found tx by txId ! ");
  282. }
  283. /**
  284. * 从 DB 从恢复区块链数据
  285. *
  286. * @return
  287. * @throws Exception
  288. */
  289. public static Blockchain initBlockchainFromDB() throws Exception {
  290. String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();
  291. if (lastBlockHash == null) {
  292. throw new Exception("ERROR: Fail to init blockchain from db. ");
  293. }
  294. return new Blockchain(lastBlockHash);
  295. }
  296. /**
  297. * 进行交易签名
  298. *
  299. * @param tx 交易数据
  300. * @param privateKey 私钥
  301. */
  302. public void signTransaction(Transaction tx, BCECPrivateKey privateKey) throws Exception {
  303. // 先来找到这笔新的交易中,交易输入所引用的前面的多笔交易的数据
  304. Map prevTxMap = new HashMap();
  305. for (TXInput txInput : tx.getInputs()) {
  306. Transaction prevTx = this.findTransaction(txInput.getTxId());
  307. prevTxMap.put(Hex.encodeHexString(txInput.getTxId()), prevTx);
  308. }
  309. tx.sign(privateKey, prevTxMap);
  310. }
  311. /**
  312. * 交易签名验证
  313. *
  314. * @param tx
  315. */
  316. private boolean verifyTransactions(Transaction tx) throws Exception {
  317. if (tx.isCoinbase()) {
  318. return true;
  319. }
  320. Map prevTx = new HashMap();
  321. for (TXInput txInput : tx.getInputs()) {
  322. Transaction transaction = this.findTransaction(txInput.getTxId());
  323. prevTx.put(Hex.encodeHexString(txInput.getTxId()), transaction);
  324. }
  325. try {
  326. return tx.verify(prevTx);
  327. } catch (Exception e) {
  328. throw new Exception("Fail to verify transaction ! transaction invalid ! ");
  329. }
  330. }
  331. }
复制代码
[h3]3.2.5 修改
  1. blockchain.sh
复制代码
脚本文件[/h3]最后修改
  1. blockchain.sh
复制代码
脚本文件,修改后内容如下:
  1. #!/bin/bash
  2. set -e
  3. # Check if the jar has been built.
  4. if [ ! -e target/part7_Signature-jar-with-dependencies.jar ]; then
  5. echo "Compiling blockchain project to a JAR"
  6. mvn package -DskipTests
  7. fi
  8. java -jar target/part7_Signature-jar-with-dependencies.jar "$@"
复制代码
[h1]4. 数字签名讲解[/h1][h2]4.1 交易过程[/h2]一笔交易就是一个地址的比特币,转移到另一个地址。由于比特币的交易记录全部都是公开的,哪个地址拥有多少比特币,都是可以查到的。因此,支付方是否拥有足够的比特币,完成这笔交易,这是可以轻易验证的。
问题出在怎么防止其他人,冒用你的名义申报交易。举例来说,有人申报了一笔交易:地址 A 向地址 B 支付10个比特币。我们怎么知道这个申报是真的,数据是正确的?
比特币协议规定,申报交易的时候,除了交易金额,转出比特币的一方还必须提供以下数据。
  • 上一笔交易的 Hash(你从哪里得到这些比特币)
  • 本次交易双方的地址
  • 支付方的公钥
  • 支付方的私钥生成的数字签名
验证这笔交易是否属实,需要三步。
  • 第一步,找到上一笔交易,确认支付方的比特币来源。
  • 第二步,算出支付方公钥的指纹,确认与支付方的地址一致,从而保证公钥属实。
  • 第三步,使用公钥去解开数字签名,保证签名属实、私钥属实。
  • 经过上面三步,就可以认定这笔交易是真实的。


[h2]4.1 数字签名[/h2]好了,现在我们已经知道了在比特币中证明用户身份的是私钥。而为了保证交易有效性,需要使用数字签名。接下来我们详细谈一下数字签名。
[h3]4.1.1 数字签名的概念[/h3]所谓数字签名(Digital Signature)(又称公开密钥数字签名、电子签章)。是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术实现,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。
[h3]4.1.2 数字签名如何工作[/h3]数字签名由两部分组成:第一部分是使用私钥(签名密钥)从消息(交易)创建签名的算法; 第二部分是允许任何人验证签名的算法。


[h3]4.1.3 数字签名的特性[/h3]1、签名不可伪造性;
2、签名不可抵赖的(简直通俗易懂~);
3、签名可信性,签名的识别和应用相对容易,任何人都可以验证签名的有效性;
4、签名是不可复制的,签名与原文是不可分割的整体;
5、签名消息不可篡改,因为任意比特数据被篡改,其签名便被随之改变,那么任何人可以验证而拒绝接受此签名。
[h3]4.1.4 设计数字签名[/h3]为了对数据进行签名,我们需要下面两样东西:
  • 要签名的数据
  • 私钥
应用签名算法可以生成一个签名,并且这个签名会被存储在交易输入中。为了对一个签名进行验证,我们需要以下三样东西:
  • 被签名的数据
  • 签名
  • 公钥
简单来说,验证过程可以被描述为:检查签名是由被签名数据加上私钥得来,并且公钥恰好是由该私钥生成。
  1. 数据签名并不是加密,你无法从一个签名重新构造出数据。这有点像哈希:
  2. 你在数据上运行一个哈希算法,然后得到一个该数据的唯一表示。签名与哈
  3. 希的区别在于密钥对:有了密钥对,才有签名验证。但是密钥对也可以被用
  4. 于加密数据:私钥用于加密,公钥用于解密数据。不过比特币并不使用加密算法。
复制代码
在比特币中,每一笔交易输入都会由创建交易的人签名。在被放入到一个块之前,必须要对每一笔交易进行验证。除了一些其他步骤,验证意味着:
  • 检查交易输入有权使用来自之前交易的输出
  • 检查交易签名是正确的
如图,对数据进行签名和对签名进行验证的过程大致如下:


现在来回顾一个交易完整的生命周期:
  • 起初,创世块里面包含了一个 coinbase 交易。在 coinbase 交易中,没有输入,所以也就不需要签名。coinbase 交易的输出包含了一个哈希过的公钥(使用的是 RIPEMD16(SHA256(PubKey)) 算法)
  • 当一个人发送币时,就会创建一笔交易。这笔交易的输入会引用之前交易的输出。每个输入会存储一个公钥(没有被哈希)和整个交易的一个签名。
  • 比特币网络中接收到交易的其他节点会对该交易进行验证。除了一些其他事情,他们还会检查:在一个输入中,公钥哈希与所引用的输出哈希相匹配(这保证了发送方只能花费属于自己的币);签名是正确的(这保证了交易是由币的实际拥有者所创建)。
  • 当一个矿工准备挖一个新块时,他会将交易放到块中,然后开始挖矿。
  • 当新块被挖出来以后,网络中的所有其他节点会接收到一条消息,告诉其他人这个块已经被挖出并被加入到区块链。
  • 当一个块被加入到区块链以后,交易就算完成,它的输出就可以在新的交易中被引用。
[h2]4.2 实现签名[/h2]交易必须被签名,因为这是比特币里面保证发送方不会花费属于其他人的币的唯一方式。如果一个签名是无效的,那么这笔交易就会被认为是无效的,因此,这笔交易也就无法被加到区块链中。
我们现在离实现交易签名还差一件事情:用于签名的数据。一笔交易的哪些部分需要签名?又或者说,要对完整的交易进行签名?选择签名的数据相当重要。因为用于签名的这个数据,必须要包含能够唯一识别数据的信息。比如,如果仅仅对输出值进行签名并没有什么意义,因为签名不会考虑发送方和接收方。
考虑到交易解锁的是之前的输出,然后重新分配里面的价值,并锁定新的输出,那么必须要签名以下数据:
  • 存储在已解锁输出的公钥哈希。它识别了一笔交易的“发送方”。
  • 存储在新的锁定输出里面的公钥哈希。它识别了一笔交易的“接收方”。
  • 新的输出值。
  1. 在比特币中,锁定/解锁逻辑被存储在脚本中,它们被分别存储在输入和输
  2. 出的 ScriptSig 和 ScriptPubKey 字段。由于比特币允许这样不同类型
  3. 的脚本,它对 ScriptPubKey 的整个内容进行了签名。
复制代码
可以看到,我们不需要对存储在输入里面的公钥签名。因此,在比特币里, 所签名的并不是一个交易,而是一个去除部分内容的输入副本,输入里面存储了被引用输出的
  1. ScriptPubKey
复制代码

看着有点复杂,来开始写代码吧。先从修改
  1. TXInput
复制代码
  1. TXOutput
复制代码
的结构开始:
  1. transaction
复制代码
包下,修改
  1. TXInput.go
复制代码
文件,修改后代码如下:
  1. public class TXInput {
  2. /**
  3. * 交易Id的hash值
  4. */
  5. private byte[] txId;
  6. /**
  7. * 交易输出索引
  8. */
  9. private int txOutputIndex;
  10. /**
  11. * 签名
  12. */
  13. private byte[] signature;
  14. /**
  15. * 公钥
  16. */
  17. private byte[] pubKey;
  18. /**
  19. * 检查公钥hash是否用于交易输入
  20. *
  21. * @param pubKeyHash
  22. * @return
  23. */
  24. public boolean usesKey(byte[] pubKeyHash) {
  25. byte[] lockingHash = AddressUtils.ripeMD160Hash(this.getPubKey());
  26. return Arrays.equals(lockingHash, pubKeyHash);
  27. }
  28. }
复制代码
在这里,我们需要重新设置两个字段,
  1. signature
复制代码
表示数字签名,
  1. pubKey
复制代码
表示原始公钥(就是钱包里面的公钥)。
接下来,我们修改
  1. TXOutput.java
复制代码
文件,修改后代码如下:
  1. public class TXOutput {
  2. /**
  3. * 数值金额
  4. */
  5. private int value
  6. /**
  7. * 公钥Hash
  8. */
  9. private byte[] pubKeyHash;
  10. /**
  11. * 检查交易输出是否能够使用指定的公钥
  12. *
  13. * @param pubKeyHash
  14. * @return
  15. */
  16. public boolean isLockedWithKey(byte[] pubKeyHash) {
  17. return Arrays.equals(this.getPubKeyHash(), pubKeyHash);
  18. }
  19. }
复制代码
因为在给
  1. TXInput
复制代码
设置签名需要用到该
  1. TXInput
复制代码
对应的
  1. TXOutput
复制代码
的数据,所以要找到这个
  1. TXOutput
复制代码
所在的
  1. Transaction
复制代码
。现在我们修改
  1. Blockchain.java
复制代码
文件,添加一个方法
  1. FindTransactionByTxID()
复制代码
  1. /**
  2. * 依据交易ID查询交易信息
  3. *
  4. * @param txId 交易ID
  5. * @return
  6. */
  7. private Transaction findTransaction(byte[] txId) throws Exception {
  8. for (BlockchainIterator iterator = this.getBlockchainIterator(); iterator.hashNext(); ) {
  9. Block block = iterator.next();
  10. for (Transaction tx : block.getTransactions()) {
  11. if (Arrays.equals(tx.getTxId(), txId)) {
  12. return tx;
  13. }
  14. }
  15. }
  16. throw new Exception("ERROR: Can not found tx by txId ! ");
  17. }
复制代码
在查找的时候,需要遍历每个区块查找里面的
  1. Transaction
复制代码
,根据
  1. txId
复制代码
判断该
  1. Transaction
复制代码
是否是我们要找的
  1. Transaction
复制代码

接下来在
  1. Blockchain.java
复制代码
文件中,继续添加方法,表示签名一笔交易:
  1. /**
  2. * 进行交易签名
  3. *
  4. * @param tx 交易数据
  5. * @param privateKey 私钥
  6. */
  7. public void signTransaction(Transaction tx, BCECPrivateKey privateKey) throws Exception {
  8. // 先来找到这笔新的交易中,交易输入所引用的前面的多笔交易的数据
  9. Map prevTxMap = new HashMap();
  10. for (TXInput txInput : tx.getInputs()) {
  11. Transaction prevTx = this.findTransaction(txInput.getTxId());
  12. prevTxMap.put(Hex.encodeHexString(txInput.getTxId()), prevTx);
  13. }
  14. tx.sign(privateKey, prevTxMap);
  15. }
复制代码
其实签名交易,就是给交易中的每个
  1. TXInput
复制代码
,设置
  1. signature
复制代码
字段。所以接下来在
  1. Transaction.java
复制代码
文件中,添加签名方法,代码如下:
  1. /**
  2. * 签名
  3. *
  4. * @param privateKey 私钥
  5. * @param prevTxMap 前面多笔交易集合
  6. */
  7. public void sign(BCECPrivateKey privateKey, Map prevTxMap) throws Exception {
  8. // coinbase 交易信息不需要签名,因为它不存在交易输入信息
  9. if (this.isCoinbase()) {
  10. return;
  11. }
  12. // 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据
  13. for (TXInput txInput : this.getInputs()) {
  14. if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) {
  15. throw new Exception("ERROR: Previous transaction is not correct");
  16. }
  17. }
  18. // 创建用于签名的交易信息的副本
  19. Transaction txCopy = this.trimmedCopy();
  20. Security.addProvider(new BouncyCastleProvider());
  21. Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
  22. ecdsaSign.initSign(privateKey);
  23. for (int i = 0; i < txCopy.getInputs().length; i++) {
  24. TXInput txInputCopy = txCopy.getInputs()[i];
  25. // 获取交易输入TxID对应的交易数据
  26. Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId()));
  27. // 获取交易输入所对应的上一笔交易中的交易输出
  28. TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()];
  29. txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
  30. txInputCopy.setSignature(null);
  31. // 得到要签名的数据
  32. byte[] signData = txCopy.getData();
  33. txInputCopy.setPubKey(null);
  34. // 对整个交易信息仅进行签名
  35. ecdsaSign.update(signData);
  36. byte[] signature = ecdsaSign.sign();
  37. // 将整个交易数据的签名赋值给交易输入,因为交易输入需要包含整个交易信息的签名
  38. // 注意是将得到的签名赋值给原交易信息中的交易输入
  39. this.getInputs()[i].setSignature(signature);
  40. }
  41. }
复制代码
这个方法接受一个私钥和一个之前交易的
  1. map
复制代码
。正如上面提到的,为了对一笔交易进行签名,我们需要获取交易输入所引用的输出,因为我们需要存储这些输出的交易。
这个方法,是签名的核心方法,我们来一步一步地分析该方法:
  1. // coinbase 交易信息不需要签名,因为它不存在交易输入信息
  2. if (this.isCoinbase()) {
  3. return;
  4. }
复制代码
  1. coinbase
复制代码
交易因为没有实际输入,所以没有被签名。
  1. // 创建用于签名的交易信息的副本
  2. Transaction txCopy = this.trimmedCopy();
复制代码
将会被签署的是修剪后的交易副本,而不是一个完整交易,接下来添加一个方法,用于拷贝一个交易,代码如下:
  1. /**
  2. * 创建用于签名的交易数据副本,交易输入的 signature 和 pubKey 需要设置为null
  3. *
  4. * @return
  5. */
  6. public Transaction trimmedCopy() {
  7. TXInput[] tmpTXInputs = new TXInput[this.getInputs().length];
  8. for (int i = 0; i < this.getInputs().length; i++) {
  9. TXInput txInput = this.getInputs()[i];
  10. tmpTXInputs[i] = new TXInput(txInput.getTxId(), txInput.getTxOutputIndex(), null, null);
  11. }
  12. TXOutput[] tmpTXOutputs = new TXOutput[this.getOutputs().length];
  13. for (int i = 0; i < this.getOutputs().length; i++) {
  14. TXOutput txOutput = this.getOutputs()[i];
  15. tmpTXOutputs[i] = new TXOutput(txOutput.getValue(), txOutput.getPubKeyHash());
  16. }
  17. return new Transaction(this.getTxId(), tmpTXInputs, tmpTXOutputs);
  18. }
复制代码
这个副本包含了所有的输入和输出,但是
  1. TXInput.signature
复制代码
  1. TXIput.pubKey
复制代码
被设置为
  1. null
复制代码

然后我们需要根据私钥设置签名对象:
  1. Security.addProvider(new BouncyCastleProvider());
  2. Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
  3. ecdsaSign.initSign(privateKey);
复制代码
接下来,我们会迭代副本中每一个输入:
  1. for (int i = 0; i < txCopy.getInputs().length; i++) {
  2. TXInput txInputCopy = txCopy.getInputs()[i];
  3. // 获取交易输入TxID对应的交易数据
  4. Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId()));
  5. // 获取交易输入所对应的上一笔交易中的交易输出
  6. TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()];
  7. txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
  8. txInputCopy.setSignature(null);
  9. ...
  10. }
复制代码
在每个输入中,
  1. signature
复制代码
被设置为
  1. null
复制代码
(仅仅是一个双重检验),
  1. pubcKey
复制代码
被设置为所引用输出的
  1. PubKeyHash
复制代码
。现在,除了当前交易,其他所有交易都是“空的”,也就是说他们的
  1. signature
复制代码
  1. pubKey
复制代码
字段被设置为
  1. null
复制代码
。因此,输入是被分开签名的,尽管这对于我们的应用并不十分紧要,但是比特币允许交易包含引用了不同地址的输入。
  1. // 得到要签名的数据
  2. byte[] signData = txCopy.getData();
  3. txInputCopy.setPubKey(null);
复制代码
  1. getData()
复制代码
方法对交易进行序列化,并使用 SHA-256 算法进行哈希。哈希后的结果就是我们要签名的数据。在获取完哈希,我们应该重置
  1. PublicKey
复制代码
字段,以便于它不会影响后面的迭代。
现在,关键点:
  1. // 对整个交易信息仅进行签名
  2. ecdsaSign.update(signData);
  3. byte[] signature = ecdsaSign.sign();
  4. // 将整个交易数据的签名赋值给交易输入,因为交易输入需要包含整个交易信息的签名
  5. // 注意是将得到的签名赋值给原交易信息中的交易输入
  6. this.getInputs()[i].setSignature(signature);
复制代码
我们对 data 进行签名。一个 ECDSA 签名就是一对数字,我们对这对数字连接起来,并存储在输入的
  1. Signature
复制代码
字段。
[h2]4.3 签名验证[/h2]现在,验证函数:
  1. Transaction.java
复制代码
文件中,添加验证签名方法,代码如下:
  1. /**
  2. * 验证交易信息
  3. *
  4. * @param prevTxMap 前面多笔交易集合
  5. * @return
  6. */
  7. public boolean verify(Map prevTxMap) throws Exception {
  8. // coinbase 交易信息不需要签名,也就无需验证
  9. if (this.isCoinbase()) {
  10. return true;
  11. }
  12. // 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据
  13. for (TXInput txInput : this.getInputs()) {
  14. if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) {
  15. throw new Exception("ERROR: Previous transaction is not correct");
  16. }
  17. }
  18. // 创建用于签名验证的交易信息的副本
  19. Transaction txCopy = this.trimmedCopy();
  20. Security.addProvider(new BouncyCastleProvider());
  21. ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1");
  22. KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);
  23. Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
  24. for (int i = 0; i < this.getInputs().length; i++) {
  25. TXInput txInput = this.getInputs()[i];
  26. // 获取交易输入TxID对应的交易数据
  27. Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId()));
  28. // 获取交易输入所对应的上一笔交易中的交易输出
  29. TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()];
  30. TXInput txInputCopy = txCopy.getInputs()[i];
  31. txInputCopy.setSignature(null);
  32. txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
  33. // 得到要签名的数据
  34. byte[] signData = txCopy.getData();
  35. txInputCopy.setPubKey(null);
  36. // 使用椭圆曲线 x,y 点去生成公钥Key
  37. BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33));
  38. BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65));
  39. ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y);
  40. ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters);
  41. PublicKey publicKey = keyFactory.generatePublic(keySpec);
  42. ecdsaVerify.initVerify(publicKey);
  43. ecdsaVerify.update(signData);
  44. if (!ecdsaVerify.verify(txInput.getSignature())) {
  45. return false;
  46. }
  47. }
  48. return true;
  49. }
复制代码
这个方法十分直观。首先,我们需要同一笔交易的副本:
  1. // 创建用于签名验证的交易信息的副本
  2. Transaction txCopy = this.trimmedCopy();
复制代码
然后,我们需要相同的签名对象:
  1. Security.addProvider(new BouncyCastleProvider());
  2. ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1");
  3. KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);
  4. Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
复制代码
接下来,我们检查每个输入中的签名:
  1. for (int i = 0; i < this.getInputs().length; i++) {
  2. TXInput txInput = this.getInputs()[i];
  3. // 获取交易输入TxID对应的交易数据
  4. Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId()));
  5. // 获取交易输入所对应的上一笔交易中的交易输出
  6. TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()];
  7. TXInput txInputCopy = txCopy.getInputs()[i];
  8. txInputCopy.setSignature(null);
  9. txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
  10. // 得到要签名的数据
  11. byte[] signData = txCopy.getData();
  12. txInputCopy.setPubKey(null);
复制代码
这个部分跟
  1. sign()
复制代码
方法一模一样,因为在验证阶段,我们需要的是与签名相同的数据。
  1. // 使用椭圆曲线 x,y 点去生成公钥Key
  2. BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33));
  3. BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65));
  4. ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y);
复制代码
这里我们解包存储在
  1. TXInput.publicKey
复制代码
中的值,因为一个公钥就是一对坐标。我们之前为了存储将它们连接在一起,现在我们需要对它们进行解包在
  1. initVerify()
复制代码
函数中使用。
  1. ...
  2. ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters);
  3. PublicKey publicKey = keyFactory.generatePublic(keySpec);
  4. ecdsaVerify.initVerify(publicKey);
  5. ecdsaVerify.update(signData);
  6. if (!ecdsaVerify.verify(txInput.getSignature())) {
  7. return false;
  8. }
  9. return true;
  10. }
复制代码
在这里:我们使用从输入提取的公钥用于设置
  1. ecdsaVerify
复制代码
的初始化信息,并且设置了要验证的签名的数据。然后来验证签名。如果所有的输入都被验证,返回
  1. true
复制代码
;如果有任何一个验证失败,返回
  1. false
复制代码
.
接下来,我们在
  1. Blockchain.java
复制代码
中添加验证方法
  1. verifyTransactions()
复制代码
  1. /**
  2. * 交易签名验证
  3. *
  4. * @param tx
  5. */
  6. private boolean verifyTransactions(Transaction tx) throws Exception {
  7. Map prevTx = new HashMap();
  8. for (TXInput txInput : tx.getInputs()) {
  9. Transaction transaction = this.findTransaction(txInput.getTxId());
  10. prevTx.put(Hex.encodeHexString(txInput.getTxId()), transaction);
  11. }
  12. try {
  13. return tx.verify(prevTx);
  14. } catch (Exception e) {
  15. throw new Exception("Fail to verify transaction ! transaction invalid ! ");
  16. }
  17. }
复制代码
在一笔交易被放入一个块之前进行验证,所以接下来我们需要修改
  1. BlockChain.java
复制代码
文件中的
  1. MineNewBlock()
复制代码
方法,
  1. /**
  2. * 打包交易,进行挖矿
  3. *
  4. * @param transactions
  5. */
  6. public void mineBlock(Transaction[] transactions) throws Exception {
  7. // 挖矿前,先验证交易记录
  8. for (Transaction tx : transactions) {
  9. if (!this.verifyTransactions(tx)) {
  10. throw new Exception("ERROR: Fail to mine block ! Invalid transaction ! ");
  11. }
  12. }
  13. String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();
  14. if (lastBlockHash == null) {
  15. throw new Exception("ERROR: Fail to get last block hash ! ");
  16. }
  17. Block block = Block.newBlock(lastBlockHash, transactions);
  18. this.addBlock(block);
  19. }
复制代码
至此,我们已经在项目中,添加了交易的签名和签名验证。
  1. main.go
复制代码
文件无需修改,接下来我们测试一下代码:还是进行创建钱包地址,创建创世区块,然后转账交易等。
在终端中输入以下命令:
  1. hanru:part7_Signature ruby$ ./blockchain.sh h
  2. hanru:part7_Signature ruby$ ./blockchain.sh createaddress
  3. hanru:part7_Signature ruby$ ./blockchain.sh createaddress
  4. hanru:part7_Signature ruby$ ./blockchain.sh printaddresses
  5. hanru:part7_Signature ruby$ ./blockchain.sh createblockchain -address 17aAQuo5A8xk9hV7NRp6Mc3ambV54gjMKX
  6. hanru:part7_Signature ruby$ ./blockchain.sh getbalance -address 17aAQuo5A8xk9hV7NRp6Mc3ambV54gjMKX
复制代码
运行结果如下:


继续输入以下命令:
  1. hanru:part7_Signature ruby$ ./blockchain.sh send -from 17aAQuo5A8xk9hV7NRp6Mc3ambV54gjMKX -to 1EGUjAdhqWTHLxDsKrMUJyNv2KMM4zxxL2 -amount 4
  2. hanru:part7_Signature ruby$ ./blockchain.sh getbalance -address 17aAQuo5A8xk9hV7NRp6Mc3ambV54gjMKX
  3. hanru:part7_Signature ruby$ ./blockchain.sh getbalance -address 1EGUjAdhqWTHLxDsKrMUJyNv2KMM4zxxL2
  4. hanru:part7_Signature ruby$ ./blockchain.sh printchain
复制代码
运行结果如下:

[h1]5. 总结[/h1]通过本章节的学习,我们知道了什么是签名,为何签名,以及如何签名。只有转账人才能生成的一段防伪造的字符串。通过验证该字符串,一方面证明该交易是转出方本人发起的,另一方面证明交易信息在传输过程中没有被更改。数字签名由:数字摘要和非对称加密技术组成。数字摘要把交易信息hash成固定长度的字符串,再用私钥对hash后的交易信息进行加密形成数字签名。交易中,需要将完整的交易信息和数字签名一起广播给矿工。矿工节点用转账人公钥对签名验证,验证成功说明该交易确实是转账人发起的;矿工节点将交易信息进行hash后与签名中的交易信息摘要进行比对,如果一致,则说明交易信息在传输过程中没有被篡改。


分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:2
帖子:2
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP