Posted on 2018-03-25 | In blockchain | | 本文总阅读量 1106次php
Words count in article: 7,996 | Reading time ≈ 33html
文章的主要思想和内容均来自 https://jeiwan.cc/posts/building-blockchain-in-go-part-5/java
在 上一篇 文章当中,咱们开始了交易机制的实现。你已经了解到交易的一些非我的特征:没有用户帐户,您的我的数据(例如:姓名、护照号码以及SSN(美国社会安全卡(Social Security Card)上的9 位数字))不是必需的,而且不存储在比特币的任何地方。但仍然必须有一些东西可以识别你是这些交易输出的全部者(例如:锁定在这些输出上的币的全部者)。这就是比特币地址的做用所在。到目前为止,咱们只是使用了任意的用户定义的字符串当作地址,如今是时候来实现真正的地址了,就像它们在比特币中实现的同样。git
这里有一个比特币地址的示例:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。这是一个很是早期的比特币地址,据称是属于中本聪的比特币地址。比特币地址是公开的。若是你想要给某人发送比特币,你须要知道对方的比特币地址。可是地址(尽管它是惟一的)并不能做为你是一个钱包全部者的凭证。事实上,这样的地址是公钥的一种可读性更好的表示 。在比特币中,你的身份是存储在你计算机上(或存储在你有权访问的其余位置)的一对(或多对)私钥和公钥。比特币依靠加密算法的组合来建立这些密钥,并保证世界上没有其余人任何人能够在没有物理访问密钥的状况下访问您的比特币。github
比特币地址与公钥不一样。比特币地址是由公钥通过单向的哈希函数生成的算法
接下来,让咱们来讨论一下这些加密算法。编程
注意:不要向本篇文章中的代码所生成的任何比特币地址发送真实的比特币来进行测试,不然后果自负……数组
公钥加密算法(public-key cryptography)使用的是密钥对:公钥和私钥。公钥属于非敏感信息,能够向任何人透露。相比之下,私钥不能公开披露:除了全部者以外,任何人都不能拥有私钥的权限,由于它是用做全部者标识的私钥。你的私钥表明就是你(固然是在加密货币世界里的)。安全
本质上,比特币钱包就是一对这样的密钥。当你安装一个钱包应用程序或者使用比特币客户端去生成一个新的地址时,它们就为你建立好了一个密钥对。在比特币种,谁控制了私钥,谁就掌握了全部发往对应公钥地址上全部比特币的控制权。网络
私钥和公钥只是随机的字节序列,所以它们不能被打印在屏幕上供人读取。这就是为何比特币会用一种算法将公钥的字节序列转化为人类可读的字符串形式。
若是你曾今使用过比特币钱包的应用程序,它可能会为你生成助记词密码短语。这些助记词能够用来替代私钥,而且可以生成私钥。这种机制是经过 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 . 如今咱们知道这个地址实际上是公钥的一种可读高的表示方式。若是咱们对他进行解码,咱们会看到公钥看起来是这样子的(字节序列的十六进制的表示方式):
1 |
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)。
让咱们以示意图的形式展现一下从公钥获得地址的过程:
所以,上述解码的公钥由三部分组成:
1 2 |
Version Public key hash Checksum 00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93 |
因为哈希函数是单向的(也就说没法逆转回去),因此不可能从一个哈希中提取公钥。不过经过执行哈希函数并进行哈希比较,咱们能够检查一个公钥是否被用于哈希的生成。
OK,如今咱们有了全部的东西,让咱们来编写一些代码。 当一些概念被写成代码时,咱们会对此理解的更加清晰和深入。
让咱们从 Wallet 的构成开始,这里咱们须要先引入一个maven包:
1 2 3 4 5 |
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.59</version> </dependency> |
钱包结构
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 |
/** * 钱包 * * @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 生成密钥对。
接着,咱们来生成比特币的钱包地址:
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 |
public class Wallet { ... /** * 获取钱包地址 * * @return */ public String getAddress() throws Exception { // 1. 获取 ripemdHashedKey byte[] ripemdHashedKey = BtcAddressUtils.ripeMD160Hash(this.getPublicKey(); // 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.这就是为何选择适当的公钥密码算法如此重要:考虑到私钥是随机数字,产生相同数字的机会必须尽量低。 理想状况下,它必须低至“永不”。
另外,须要注意的是你不须要链接到比特币的节点上去获取比特币的地址。有关地址生成的开源算法工具包已经有不少编程语言和库实现了。
如今,咱们须要去修改交易输入与输出,让他们开始使用真实的地址:
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 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 42 43 44 45 |
/** * 交易输出 * * @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字段中。
如今,让咱们一块儿来检查一下是否可以正常运行:
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 |
$ ./blochchain.sh createwallet wallet address : 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh $ ./blochchain.sh createwallet wallet address : 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e $ ./blochchain.sh createwallet wallet address : 19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1 $ ./blochchain.sh createblockchain -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh Elapsed Time: 6.77 seconds correct hash Hex: 00000e44be0c94c39a4fef24c67d85c428e8bfbd227e292d75c0f4d398e2e81c Done ! $ ./blochchain.sh getbalance -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh Balance of '13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh': 10 $ ./blochchain.sh send -from 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e -to 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVd -amount 5 java.lang.Exception: ERROR: Not enough funds $ ./blochchain.sh send -from 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh -to 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e-amount 5 Elapsed Time: 4.477 seconds correct hash Hex: 00000da41dfacc8032a553ed5b1aa5e24318d5d89ca14a16c4f70129609c8365 Success! $ ./blochchain.sh getbalance -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh Balance of '13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh': 5 $ ./blochchain.sh getbalance -address 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e Balance of '1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e': 5 $ ./blochchain.sh getbalance -address 19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1 Balance of '19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1': 0 |
Nice! 如今让咱们一块儿来实现交易签名部分的内容。
交易数据必须被签名,由于这是比特币中可以保证不能花费属于他人比特币的惟一方法。若是一个签名是无效的,那么这笔交易也是无效的,这样的话,这笔交易就不能被添加到区块链中去。
咱们已经有了实现交易签名的全部片断,还有一个事情除外:用于签名的数据。交易数据中哪一部分是真正用于签名的呢?难道是所有数据?选择用于签名的数据至关的重要。用于签名的数据必须包含以独特且惟一的方式标识数据的信息。例如,仅对交易输出签名是没有意义的,由于此签名不会考虑发送发与接收方。
考虑到交易数据要解锁前面的交易输出,从新分配交易输出中的 value 值,而且锁定新的交易输出,所以下面这些数据是必须被签名的:
在比特币中,锁定/解锁逻辑存储在脚本中,解锁脚本存储在交易输入的 ScriptSig 字段中,而锁定脚本存储在交易输出的 ScriptPubKey 的字段中。 因为比特币容许不一样类型的脚本,所以它会对ScriptPubKey的所有内容进行签名。
如你所见,咱们不须要去对存储在交易输入中的公钥进行签名。正由于如此,在比特币中,所签名的并非一个交易,而是一个去除部份内容的交易输入副本,交易输入里面存储了被引用交易输出的 ScriptPubKey
。
获取修剪后的交易副本的详细过程在这里. 虽然它可能已通过时了,可是我并无找到另外一个更可靠的来源。
OK,它看起来有点复杂,所以让咱们来开始coding吧。咱们将从 Sign 方法开始:
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 |
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这个方法:
1 2 3 |
if (this.isCoinbase()) { return; } |
因为 coinbase 交易信息不存在交易输入信息,所以它不须要签名,直接return.
1 |
Transaction txCopy = this.trimmedCopy(); |
建立交易的副本
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 |
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
签名算法:
1 2 3 |
Security.addProvider(new BouncyCastleProvider()); Signature ecdsaSign = Signature.getInstance("SHA256withECDSA",BouncyCastleProvider.PROVIDER_NAME); ecdsaSign.initSign(privateKey); |
接下来,咱们迭代交易副本中的交易输入:
1 2 3 4 5 6 7 |
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
字段,以便于它不会影响后面的迭代。
1 2 3 |
// 获得要签名的数据,即交易ID txCopy.setTxId(txCopy.hash()); txInput.setPubKey(null); |
如今,最关键的部分来了:
1 2 3 4 5 6 7 8 9 10 |
// 对整个交易信息仅进行签名,即对交易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进行签名,从而获得了交易输入所要设置的交易签名。
如今,让咱们来实现交易的验证功能:
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 |
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; } ... } |
首选,同前面签名同样,咱们先获取交易的拷贝数据:
1 |
Transaction txCopy = this.trimmedCopy(); |
获取椭圆曲线参数和签名类:
1 2 3 4 |
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 5 6 7 8 9 10 11 12 13 14 |
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方法中的相同,由于在验证过程当中咱们须要签署相同的数据。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 使用椭圆曲线 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 的一种方法:
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 |
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 中:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 |
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,让咱们再一次对整个工程的代码作一个测试,测试结果:
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 |
$ ./blochchain.sh createwallet wallet address : 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 $ ./blochchain.sh createwallet wallet address : 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB $ ./blochchain.sh createwallet wallet address : 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f $ ./blochchain.sh createblockchain -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 Elapsed Time: 164.961 seconds correct hash Hex: 00000524231ae1832c49957848d2d1871cc35ff4d113c23be1937c6dff5cdf2a Done ! $ ./blochchain.sh getbalance -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 Balance of '1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6': 10 $ ./blochchain.sh send -from 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB -to 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f -amount 5 java.lang.Exception: ERROR: Not enough funds $ ./blochchain.sh send -from 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 -to 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB -amount 5 Elapsed Time: 54.92 seconds correct hash Hex: 00000354f86cde369d4c39d2b3016ac9a74956425f1348b4c26b2cddb98c100b Success! $ ./blochchain.sh getbalance -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 Balance of '1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6': 5 $ ./blochchain.sh getbalance -address 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB Balance of '1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB': 5 $ ./blochchain.sh getbalance -address 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f Balance of '13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f': 0 |
Good!没有任何错误!
让咱们注释掉 NewUTXOTransaction
方法中的一行代码,确保未被签名的交易不能被添加到区块中:
1 2 3 4 5 |
... // blockchain.signTransaction(newTx, senderWallet.getPrivateKey()); ... |
测试结果:
1 2 3 4 5 6 |
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) |
说明
WalletUtils 如若抛出异常:Illegal key size or default parameters,请按如下方法进行解决:
https://stackoverflow.com/questions/6481627/java-security-illegal-key-size-or-default-parameters
这一节,咱们学到了:
到目前为止,咱们已经实现了比特币的许多关键特性! 咱们已经实现了除外网络外的几乎全部功能,而且在下一篇文章中,咱们将继续完善交易这一环节机制。