资讯专栏INFORMATION COLUMN

基于Java语言构建区块链(六)—— 交易(Merkle Tree)

KevinYan / 706人阅读

摘要:截止年月号,比特币中有个区块,并且这些数据占据了的磁盘空间。每个比特币节点都是路由区块链数据库挖矿钱包服务的功能集合。是比特币的轻量级节点,它不需要下载所有的区块链数据,也不需要验证区块和交易数据。

</>复制代码

  1. 最终内容请以原文为准:https://wangwei.one/posts/630...
引言

在这一系列文章的最开始部分,我们提到过区块链是一个分布式的数据库。那时候,我们决定跳过"分布式"这一环节,并且聚焦于"数据存储"这一环节。到目前为止,我们几乎实现了区块链的所有组成部分。在本篇文章中,我们将会涉及一些在前面的文章中所忽略的一些机制,并且在下一篇文章中我们将开始研究区块链的分布式特性。

前面各个部分内容:

基本原型

工作量证明

持久化 & 命令行

交易(UTXO)

地址(钱包)

UTXO池

在 持久化 & 命令行 这篇文章中,我们研究了比特币核心存储区块的方式。当中我们提到过与区块相关的数据存储在 blocks 这个数据桶中,而交易数据则存储在 chainstate 这个数据桶中,让我们来回忆一下,chainstate 数据桶的数据结构:

"c" + 32-byte transaction hash -> unspent transaction output record for that transaction

</>复制代码

  1. 某笔交易的UTXO记录

"B" -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs

</>复制代码

  1. 数据库所表示的UTXO的区块Hash

从那篇文章开始,我们已经实现了比特币的交易机制,但是我们还没有用到 chainstate 数据桶去存储我们的交易输出。所以,这将是我们现在要去做的事情。

chainstate 不会去存储交易数据。相反,它存储的是 UTXO 集,也就是未被花费的交易输出集合。除此之外,它还存储了"数据库所表示的UTXO的区块Hash",我们这里先暂且忽略这一点,因为我们还没有用到区块高度(这一点我们会在后面的文章进行实现)。

那么,我们为什么需要 UTXO 池呢?

一起来看一下我们前面实现的 findUnspentTransactions 方法:

</>复制代码

  1. /**
  2. * 查找钱包地址对应的所有未花费的交易
  3. *
  4. * @param pubKeyHash 钱包公钥Hash
  5. * @return
  6. */
  7. private Transaction[] findUnspentTransactions(byte[] pubKeyHash) throws Exception {
  8. Map allSpentTXOs = this.getAllSpentTXOs(pubKeyHash);
  9. Transaction[] unspentTxs = {};
  10. // 再次遍历所有区块中的交易输出
  11. for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) {
  12. Block block = blockchainIterator.next();
  13. for (Transaction transaction : block.getTransactions()) {
  14. String txId = Hex.encodeHexString(transaction.getTxId());
  15. int[] spentOutIndexArray = allSpentTXOs.get(txId);
  16. for (int outIndex = 0; outIndex < transaction.getOutputs().length; outIndex++) {
  17. if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) {
  18. continue;
  19. }
  20. // 保存不存在 allSpentTXOs 中的交易
  21. if (transaction.getOutputs()[outIndex].isLockedWithKey(pubKeyHash)) {
  22. unspentTxs = ArrayUtils.add(unspentTxs, transaction);
  23. }
  24. }
  25. }
  26. }
  27. return unspentTxs;
  28. }

该方法是用来查找钱包地址对应的包含未花费交易输出的交易信息。由于交易信息是存储在区块当中,所以我们现有的做法是遍历区块链中的每个区块,然后遍历每个区块中的交易信息,再然后遍历每个交易中的交易输出,并检查交易输出是否被相应的钱包地址所锁定,效率非常低下。截止2018年3月29号,比特币中有 515698 个区块,并且这些数据占据了140+Gb 的磁盘空间。这也就意味着一个人必须运行全节点(下载所有的区块数据)才能验证交易信息。此外,验证交易信息需要遍历所有的区块。

针对这个问题的解决办法是需要有一个存储了所有UTXOs(未花费交易输出)的索引,这就是 UTXOs 池所要做的事情:UTXOs池其实是一个缓存空间,它所缓存的数据需要从构建区块链中所有的交易数据中获得(通过遍历所有的区块链,不过这个构建操作只需要执行一次即可),并且它后续还会用于钱包余额的计算以及新的交易数据的验证。截止到2017年9月,UTXOs池大约为 2.7Gb。

好了,让我们来想一下,为了实现 UTXOs 池我们需要做哪些事情。当前,有下列方法被用于查找交易信息:

Blockchain.getAllSpentTXOs —— 查询所有已被花费的交易输出。它需要遍历区块链中所有区块中交易信息。

Blockchain.findUnspentTransactions —— 查询包含未被花费的交易输出的交易信息。它也需要遍历区块链中所有区块中交易信息。

Blockchain.findSpendableOutputs —— 该方法用于新的交易创建之时。它需要找到足够多的交易输出,以满足所需支付的金额。需要调用 Blockchain.findUnspentTransactions 方法。

Blockchain.findUTXO —— 查询钱包地址所对应的所有未花费交易输出,然后用于计算钱包余额。需要调用

Blockchain.findUnspentTransactions 方法。

Blockchain.findTransaction —— 通过交易ID查询交易信息。它需要遍历所有的区块直到找到交易信息为止。

如你所见,上面这些方法都需要去遍历数据库中的所有区块。由于UTXOs池只存储未被花费的交易输出,而不会存储所有的交易信息,因此我们不会对有 Blockchain.findTransaction 进行优化。

那么,我们需要下列这些方法:

Blockchain.findUTXO —— 通过遍历所有的区块来找到所有未被花费的交易输出.

UTXOSet.reindex —— 调用上面 findUTXO 方法,然后将查询结果存储在数据库中。也即需要进行缓存的地方。

UTXOSet.findSpendableOutputs —— 与 Blockchain.findSpendableOutputs 类似,区别在于会使用 UTXO 池。

UTXOSet.findUTXO —— 与Blockchain.findUTXO 类似,区别在于会使用 UTXO 池。

Blockchain.findTransaction —— 逻辑保持不变。

这样,两个使用最频繁的方法将从现在开始使用缓存!让我们开始编码吧!

定义 UTXOSet

</>复制代码

  1. @NoArgsConstructor
  2. @AllArgsConstructor
  3. @Slf4j
  4. public class UTXOSet {
  5. private Blockchain blockchain;
  6. }

重建 UTXO 池索引:

</>复制代码

  1. public class UTXOSet {
  2. ...
  3. /**
  4. * 重建 UTXO 池索引
  5. */
  6. @Synchronized
  7. public void reIndex() {
  8. log.info("Start to reIndex UTXO set !");
  9. RocksDBUtils.getInstance().cleanChainStateBucket();
  10. Map allUTXOs = blockchain.findAllUTXOs();
  11. for (Map.Entry entry : allUTXOs.entrySet()) {
  12. RocksDBUtils.getInstance().putUTXOs(entry.getKey(), entry.getValue());
  13. }
  14. log.info("ReIndex UTXO set finished ! ");
  15. }
  16. ...
  17. }

此方法用于初始化 UTXOSet。首先,需要清空 chainstate 数据桶,然后查询所有未被花费的交易输出,并将它们保存到 chainstate 数据桶中。

实现 findSpendableOutputs 方法,供 Transation.newUTXOTransaction 调用

</>复制代码

  1. public class UTXOSet {
  2. ...
  3. /**
  4. * 寻找能够花费的交易
  5. *
  6. * @param pubKeyHash 钱包公钥Hash
  7. * @param amount 花费金额
  8. */
  9. public SpendableOutputResult findSpendableOutputs(byte[] pubKeyHash, int amount) {
  10. Map unspentOuts = Maps.newHashMap();
  11. int accumulated = 0;
  12. Map chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket();
  13. for (Map.Entry entry : chainstateBucket.entrySet()) {
  14. String txId = entry.getKey();
  15. TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(entry.getValue());
  16. for (int outId = 0; outId < txOutputs.length; outId++) {
  17. TXOutput txOutput = txOutputs[outId];
  18. if (txOutput.isLockedWithKey(pubKeyHash) && accumulated < amount) {
  19. accumulated += txOutput.getValue();
  20. int[] outIds = unspentOuts.get(txId);
  21. if (outIds == null) {
  22. outIds = new int[]{outId};
  23. } else {
  24. outIds = ArrayUtils.add(outIds, outId);
  25. }
  26. unspentOuts.put(txId, outIds);
  27. if (accumulated >= amount) {
  28. break;
  29. }
  30. }
  31. }
  32. }
  33. return new SpendableOutputResult(accumulated, unspentOuts);
  34. }
  35. ...
  36. }

实现 findUTXOs 接口,供 CLI.getBalance 调用:

</>复制代码

  1. public class UTXOSet {
  2. ...
  3. /**
  4. * 查找钱包地址对应的所有UTXO
  5. *
  6. * @param pubKeyHash 钱包公钥Hash
  7. * @return
  8. */
  9. public TXOutput[] findUTXOs(byte[] pubKeyHash) {
  10. TXOutput[] utxos = {};
  11. Map chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket();
  12. if (chainstateBucket.isEmpty()) {
  13. return utxos;
  14. }
  15. for (byte[] value : chainstateBucket.values()) {
  16. TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(value);
  17. for (TXOutput txOutput : txOutputs) {
  18. if (txOutput.isLockedWithKey(pubKeyHash)) {
  19. utxos = ArrayUtils.add(utxos, txOutput);
  20. }
  21. }
  22. }
  23. return utxos;
  24. }
  25. ...
  26. }

以上这些方法都是先前 Blockchain 中相应方法的微调版,先前的方法将不再使用。

有了UTXO池之后,意味着我们的交易数据分开存储到了两个不同的数据桶中:交易数据存储到了 block 数据桶中,而UTXO存储到了 chainstate 数据桶中。这就需要一种同步机制来保证每当一个新的区块产生时,UTXO池能够及时同步最新区块中的交易数据,毕竟我们不想频地进行 reIndex 。因此,我们需要如下方法:

更新UTXO池:

</>复制代码

  1. public class UTXOSet {
  2. ...
  3. /**
  4. * 更新UTXO池
  5. *

  6. * 当一个新的区块产生时,需要去做两件事情:
  7. * 1)从UTXO池中移除花费掉了的交易输出;
  8. * 2)保存新的未花费交易输出;
  9. *
  10. * @param tipBlock 最新的区块
  11. */
  12. @Synchronized
  13. public void update(Block tipBlock) {
  14. if (tipBlock == null) {
  15. log.error("Fail to update UTXO set ! tipBlock is null !");
  16. throw new RuntimeException("Fail to update UTXO set ! ");
  17. }
  18. for (Transaction transaction : tipBlock.getTransactions()) {
  19. // 根据交易输入排查出剩余未被使用的交易输出
  20. if (!transaction.isCoinbase()) {
  21. for (TXInput txInput : transaction.getInputs()) {
  22. // 余下未被使用的交易输出
  23. TXOutput[] remainderUTXOs = {};
  24. String txId = Hex.encodeHexString(txInput.getTxId());
  25. TXOutput[] txOutputs = RocksDBUtils.getInstance().getUTXOs(txId);
  26. if (txOutputs == null) {
  27. continue;
  28. }
  29. for (int outIndex = 0; outIndex < txOutputs.length; outIndex++) {
  30. if (outIndex != txInput.getTxOutputIndex()) {
  31. remainderUTXOs = ArrayUtils.add(remainderUTXOs, txOutputs[outIndex]);
  32. }
  33. }
  34. // 没有剩余则删除,否则更新
  35. if (remainderUTXOs.length == 0) {
  36. RocksDBUtils.getInstance().deleteUTXOs(txId);
  37. } else {
  38. RocksDBUtils.getInstance().putUTXOs(txId, remainderUTXOs);
  39. }
  40. }
  41. }
  42. // 新的交易输出保存到DB中
  43. TXOutput[] txOutputs = transaction.getOutputs();
  44. String txId = Hex.encodeHexString(transaction.getTxId());
  45. RocksDBUtils.getInstance().putUTXOs(txId, txOutputs);
  46. }
  47. }
  48. ...
  49. }

让我们将 UTXOSet 用到它们所需之处去:

</>复制代码

  1. public class CLI {
  2. ...
  3. /**
  4. * 创建区块链
  5. *
  6. * @param address
  7. */
  8. private void createBlockchain(String address) {
  9. Blockchain blockchain = Blockchain.createBlockchain(address);
  10. UTXOSet utxoSet = new UTXOSet(blockchain);
  11. utxoSet.reIndex();
  12. log.info("Done ! ");
  13. }
  14. ...
  15. }

当创建一个新的区块链是,我们需要重建 UTXO 池索引。截止目前,这是唯一一处用到 reIndex 的地方,尽管看起有些多余,因为在区块链创建之初仅仅只有一个区块和一笔交易。

修改 CLI.send 接口:

</>复制代码

  1. public class CLI {
  2. ...
  3. /**
  4. * 转账
  5. *
  6. * @param from
  7. * @param to
  8. * @param amount
  9. */
  10. private void send(String from, String to, int amount) throws Exception {
  11. ...
  12. Blockchain blockchain = Blockchain.createBlockchain(from);
  13. Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain);
  14. Block newBlock = blockchain.mineBlock(new Transaction[]{transaction});
  15. new UTXOSet(blockchain).update(newBlock);
  16. ...
  17. }
  18. ...
  19. }

当一个新的区块产生后,需要去更新 UTXO 池数据。

让我们来检查一下它们的运行情况:

</>复制代码

  1. $ java -jar blockchain-java-jar-with-dependencies.jar createwallet
  2. wallet address : 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf
  3. $ java -jar blockchain-java-jar-with-dependencies.jar createwallet
  4. wallet address : 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG
  5. $ java -jar blockchain-java-jar-with-dependencies.jar createwallet
  6. wallet address : 1L1RoFgyjCrNPCPHmSEBtNiV3h2wiF9mZV
  7. $ java -jar blockchain-java-jar-with-dependencies.jar createblockchain -address 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf
  8. Elapsed Time: 164.961 seconds
  9. correct hash Hex: 00225493862611bc517cb6b3610e99d26d98a6b52484c9fa745df6ceff93f445
  10. Done !
  11. $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf
  12. Balance of "1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf": 10
  13. $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG -to 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf -amount 5
  14. java.lang.Exception: ERROR: Not enough funds
  15. $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf -to 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG -amount 2
  16. Elapsed Time: 54.92 seconds
  17. correct hash Hex: 0001ab21f71ff2d6d532bf3b3388db790c2b03e28d7bd27bd669c5f6380a4e5b
  18. Success!
  19. $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf -to 1L1RoFgyjCrNPCPHmSEBtNiV3h2wiF9mZV -amount 2
  20. Elapsed Time: 54.92 seconds
  21. correct hash Hex: 0009b925cc94e3db8bab2958b1fc2d1764aa15531e20756d92c3a93065c920f0
  22. Success!
  23. $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf
  24. Balance of "1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf": 6
  25. $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG
  26. Balance of "1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG": 2
  27. $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1L1RoFgyjCrNPCPHmSEBtNiV3h2wiF9mZV
  28. Balance of "1L1RoFgyjCrNPCPHmSEBtNiV3h2wiF9mZV": 2
奖励机制

前面的章节中我们省略了矿工挖矿的奖励机制。时机已经成熟,该实现它了。

矿工奖励其实是一个 coinbase 交易(创币交易)。当一个矿工节点开始去生产一个新的区块时,他会从队列中取出一些交易数据,并且为它们预制一个 coinbase 交易。这笔 coinbase 交易中仅有的交易输出包含了矿工的公钥hash。

只需要更新 send 命令接口,我们就可以轻松实现矿工的奖励机制:

</>复制代码

  1. public class CLI {
  2. ...
  3. /**
  4. * 转账
  5. *
  6. * @param from
  7. * @param to
  8. * @param amount
  9. */
  10. private void send(String from, String to, int amount) throws Exception {
  11. ...
  12. Blockchain blockchain = Blockchain.createBlockchain(from);
  13. // 新交易
  14. Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain);
  15. // 奖励
  16. Transaction rewardTx = Transaction.newCoinbaseTX(from, "");
  17. Block newBlock = blockchain.mineBlock(new Transaction[]{transaction, rewardTx});
  18. new UTXOSet(blockchain).update(newBlock);
  19. ...
  20. }
  21. ...
  22. }

还需要修改交易验证方法,coinbase 交易直接验证通过:

</>复制代码

  1. public class Blockchain {
  2. /**
  3. * 交易签名验证
  4. *
  5. * @param tx
  6. */
  7. private boolean verifyTransactions(Transaction tx) {
  8. if (tx.isCoinbase()) {
  9. return true;
  10. }
  11. ...
  12. }
  13. ...
  14. }

在我们的实现逻辑中,代币的发送也是区块的生产者,因此,奖励也归他所有。

让我们来验证一下奖励机制:

</>复制代码

  1. $ java -jar blockchain-java-jar-with-dependencies.jar createwallet
  2. wallet address : 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD
  3. $ java -jar blockchain-java-jar-with-dependencies.jar createwallet
  4. wallet address : 17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX
  5. $ java -jar blockchain-java-jar-with-dependencies.jar createwallet
  6. wallet address : 12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT
  7. $ java -jar blockchain-java-jar-with-dependencies.jar createblockchain -address 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD
  8. Elapsed Time: 17.973 seconds
  9. correct hash Hex: 0000defe83a851a5db3803d5013bbc20c6234f176b2c52ae36fdb53d28b33d93
  10. Done !
  11. $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD -to 17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX -amount 6
  12. Elapsed Time: 30.887 seconds
  13. correct hash Hex: 00005fd36a2609b43fd940577f93b8622e88e854f5ccfd70e113f763b6df69f7
  14. Success!
  15. $ java -jar blockchain-java-jar-with-dependencies.jar send -from 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD -to 12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT -amount 3
  16. Elapsed Time: 45.267 seconds
  17. correct hash Hex: 00009fd7c59b830b60ec21ade7672921d2fb0962a1b06a42c245450e47582a13
  18. Success!
  19. $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD
  20. Balance of "1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD": 21
  21. $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX
  22. Balance of "17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX": 6
  23. $ java -jar blockchain-java-jar-with-dependencies.jar getbalance -address 12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT
  24. Balance of "12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT": 3

1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD 这个地址一共收到了三份奖励:

第一次是开采创世区块;

第二次是开采区块:00005fd36a2609b43fd940577f93b8622e88e854f5ccfd70e113f763b6df69f7

第三次是开采区块:00009fd7c59b830b60ec21ade7672921d2fb0962a1b06a42c245450e47582a13

Merkle Tree

Merkle Tree(默克尔树) 是这篇文章中我们需要重点讨论的一个机制。

正如我前面提到的那样,整个比特币的数据库占到了大约140G的磁盘空间。由于比特币的分布式特性,网络中的每一个节点必须是独立且自给自足的。每个比特币节点都是路由、区块链数据库、挖矿、钱包服务的功能集合。每个节点都参与全网络的路由功能,同时也可能包含其他功能。每个节点都参与验证并传播交易及区块信息,发现并维持与对等节点的连接。一个全节点(full node)包括以下四个功能:

随着越来越多的人开始使用比特币,这条规则开始变得越来越难以遵循:让每一个人都去运行一个完整的节点不太现实。在中本聪发布的 比特币白皮书 中,针对这个问题提出了一个解决方案:Simplified Payment Verification (SPV)(简易支付验证)。SPV是比特币的轻量级节点,它不需要下载所有的区块链数据,也不需要验证区块和交易数据。相反,当SPV想要验证一笔交易的有效性时,它会从它所连接的全节点上检索所需要的一些数据。这种机制保证了在只有一个全节点的情况,可以运行多个SPV轻钱包节点。

</>复制代码

  1. 更多有关SPV的介绍,请查看:《精通比特币(第二版)》第八章

为了使SPV成为可能,就需要有一种方法在没有全量下载区块数据的情况下,来检查一个区块是否包含了某笔交易。这就是 Merkle Tree 发挥作用的地方了。

比特币中所使用的Merkle Tree是为了获得交易的Hash值,随后这个已经被Pow(工作量证明)系统认可了的Hash值会被保存到区块头中。到目前为止,我们只是简单地计算了一个区块中每笔交易的Hash值,然后在准备Pow数据时,再对这些交易进行 SHA-256 计算。虽然这是一个用于获取区块交易唯一表示的一个不错的途径,但是它不具有到 Merkle Tree的优点。

来看一下Merkle Tree的结构:

每一个区块都会构建一个Merkle Tree,它从最底部的叶子节点开始往上构建,每一个交易的Hash就是一个叶子节点(比特币中用的双SHA256算法)。叶子节点的数量必须是偶数个,但是并不是每一个区块都能包含偶数笔交易数据。如果存在奇数笔交易数据,那么最后一笔交易数据将会被复制一份(这仅仅发生在Merkle Tree中,而不是区块中)。

从下往上移动,叶子节点成对分组,它们的Hash值被连接到一起,并且在此基础上再次计算出新的Hash值。新的Hash 形成新的树节点。这个过程不断地被重复,直到最后仅剩一个被称为根节点的树节点。这个根节点的Hash就是区块中交易数据们的唯一代表,它会被保存到区块头中,并被用于参与POW系统的计算。

Merkle树的好处是节点可以在不下载整个块的情况下验证某笔交易的合法性。 为此,只需要交易Hash,Merkle树根Hash和Merkle路径。

Merkle Tree代码实现如下:

</>复制代码

  1. package one.wangwei.blockchain.transaction;
  2. import com.google.common.collect.Lists;
  3. import lombok.Data;
  4. import one.wangwei.blockchain.util.ByteUtils;
  5. import org.apache.commons.codec.digest.DigestUtils;
  6. import java.util.List;
  7. /**
  8. * 默克尔树
  9. *
  10. * @author wangwei
  11. * @date 2018/04/15
  12. */
  13. @Data
  14. public class MerkleTree {
  15. /**
  16. * 根节点
  17. */
  18. private Node root;
  19. /**
  20. * 叶子节点Hash
  21. */
  22. private byte[][] leafHashes;
  23. public MerkleTree(byte[][] leafHashes) {
  24. constructTree(leafHashes);
  25. }
  26. /**
  27. * 从底部叶子节点开始往上构建整个Merkle Tree
  28. *
  29. * @param leafHashes
  30. */
  31. private void constructTree(byte[][] leafHashes) {
  32. if (leafHashes == null || leafHashes.length < 1) {
  33. throw new RuntimeException("ERROR:Fail to construct merkle tree ! leafHashes data invalid ! ");
  34. }
  35. this.leafHashes = leafHashes;
  36. List parents = bottomLevel(leafHashes);
  37. while (parents.size() > 1) {
  38. parents = internalLevel(parents);
  39. }
  40. root = parents.get(0);
  41. }
  42. /**
  43. * 构建一个层级节点
  44. *
  45. * @param children
  46. * @return
  47. */
  48. private List internalLevel(List children) {
  49. List parents = Lists.newArrayListWithCapacity(children.size() / 2);
  50. for (int i = 0; i < children.size() - 1; i += 2) {
  51. Node child1 = children.get(i);
  52. Node child2 = children.get(i + 1);
  53. Node parent = constructInternalNode(child1, child2);
  54. parents.add(parent);
  55. }
  56. // 内部节点奇数个,只对left节点进行计算
  57. if (children.size() % 2 != 0) {
  58. Node child = children.get(children.size() - 1);
  59. Node parent = constructInternalNode(child, null);
  60. parents.add(parent);
  61. }
  62. return parents;
  63. }
  64. /**
  65. * 底部节点构建
  66. *
  67. * @param hashes
  68. * @return
  69. */
  70. private List bottomLevel(byte[][] hashes) {
  71. List parents = Lists.newArrayListWithCapacity(hashes.length / 2);
  72. for (int i = 0; i < hashes.length - 1; i += 2) {
  73. Node leaf1 = constructLeafNode(hashes[i]);
  74. Node leaf2 = constructLeafNode(hashes[i + 1]);
  75. Node parent = constructInternalNode(leaf1, leaf2);
  76. parents.add(parent);
  77. }
  78. if (hashes.length % 2 != 0) {
  79. Node leaf = constructLeafNode(hashes[hashes.length - 1]);
  80. // 奇数个节点的情况,复制最后一个节点
  81. Node parent = constructInternalNode(leaf, leaf);
  82. parents.add(parent);
  83. }
  84. return parents;
  85. }
  86. /**
  87. * 构建叶子节点
  88. *
  89. * @param hash
  90. * @return
  91. */
  92. private static Node constructLeafNode(byte[] hash) {
  93. Node leaf = new Node();
  94. leaf.hash = hash;
  95. return leaf;
  96. }
  97. /**
  98. * 构建内部节点
  99. *
  100. * @param leftChild
  101. * @param rightChild
  102. * @return
  103. */
  104. private Node constructInternalNode(Node leftChild, Node rightChild) {
  105. Node parent = new Node();
  106. if (rightChild == null) {
  107. parent.hash = leftChild.hash;
  108. } else {
  109. parent.hash = internalHash(leftChild.hash, rightChild.hash);
  110. }
  111. parent.left = leftChild;
  112. parent.right = rightChild;
  113. return parent;
  114. }
  115. /**
  116. * 计算内部节点Hash
  117. *
  118. * @param leftChildHash
  119. * @param rightChildHash
  120. * @return
  121. */
  122. private byte[] internalHash(byte[] leftChildHash, byte[] rightChildHash) {
  123. byte[] mergedBytes = ByteUtils.merge(leftChildHash, rightChildHash);
  124. return DigestUtils.sha256(mergedBytes);
  125. }
  126. /**
  127. * Merkle Tree节点
  128. */
  129. @Data
  130. public static class Node {
  131. private byte[] hash;
  132. private Node left;
  133. private Node right;
  134. }
  135. }

然后修改 Block.hashTransaction 接口:

</>复制代码

  1. public class Block {
  2. ...
  3. /**
  4. * 对区块中的交易信息进行Hash计算
  5. *
  6. * @return
  7. */
  8. public byte[] hashTransaction() {
  9. byte[][] txIdArrays = new byte[this.getTransactions().length][];
  10. for (int i = 0; i < this.getTransactions().length; i++) {
  11. txIdArrays[i] = this.getTransactions()[i].hash();
  12. }
  13. return new MerkleTree(txIdArrays).getRoot().getHash();
  14. }
  15. ...
  16. }

MerkleTree的根节点的Hash值,就是区块中交易信息的唯一代表。

小结

这一节我们主要是对前面的交易机制做了进一步的优化,加入UTXO池和Merkle Tree机制。

资料

源码:https://github.com/wangweiX/b...

The UTXO Set:_Data_Storage#The_UTXO_set_.28chainstate_leveldb.29)

UTXO set statistics

Merkle Tree

Why every Bitcoin user should understand “SPV security”

Script

“Ultraprune” Bitcoin Core commit

Smart contracts and Bitcoin

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/24038.html

相关文章

  • 基于Java语言构建区块)—— 交易Merkle Tree

    摘要:截止年月号,比特币中有个区块,并且这些数据占据了的磁盘空间。每个比特币节点都是路由区块链数据库挖矿钱包服务的功能集合。是比特币的轻量级节点,它不需要下载所有的区块链数据,也不需要验证区块和交易数据。 showImg(https://img.i7years.com/blog/pexels-photo-38136.jpeg); 最终内容请以原文为准:https://wangwei.one/...

    liuhh 评论0 收藏0
  • CITA 是如何达到 15000 TPS 的?

    摘要:本文将会简要讨论秘猿科技是如何对进行性能优化的。在区块链中,的共识是一个连续的共识。预处理在传统的类共识的区块链中,共识和交易的处理都是串行的。在共识的过程中,是闲置的。减少不必要的消息。例如,在服务收到时,需要对其合法性进行验证。 在前两期中,秘猿小课堂给大家分享了构建高性能区块链内核 CITA 背后的思考。这一期,我们深入研究 CITA 是如何进行性能优化,并且将交易处理的性能达到...

    BLUE 评论0 收藏0
  • 基于Java语言构建区块(一)—— 基本原型

    摘要:本文将基于语言构建简化版的,来实现数字货币。值用于确保的安全。计算是计算敏感的操作,即使在高性能电脑也需要花费一段时间来完成计算这也就是为什么人们购买高性能进行比特币挖矿的原因。资料源代码精通比特币第二版 showImg(https://segmentfault.com/img/remote/1460000013923206?w=1600&h=900); 最终内容请以原文为准:http...

    PiscesYE 评论0 收藏0
  • 基于Java语言构建区块(一)—— 基本原型

    摘要:本文将基于语言构建简化版的,来实现数字货币。值用于确保的安全。计算是计算敏感的操作,即使在高性能电脑也需要花费一段时间来完成计算这也就是为什么人们购买高性能进行比特币挖矿的原因。资料源代码精通比特币第二版 showImg(https://segmentfault.com/img/remote/1460000013923206?w=1600&h=900); 最终内容请以原文为准:http...

    Flink_China 评论0 收藏0
  • 深入理解Plasma(四)Plasma Cash

    摘要:稀疏梅克尔树在上文中我们已经了解到一个交易的成功的前提是需要发送方提供关于一个的完整交易历史。因此,中使用了一种称为稀疏梅克尔树,的数据结构存储交易数据,能够在的时间复杂度验证一个交易不存在。 本文首发于深入浅出区块链社区原文链接:深入理解Plasma(四)Plasma Cash原文已更新,请读者前往原文阅读 这一系列文章将围绕以太坊的二层扩容框架 Plasma,介绍其基本运行原理,具...

    godiscoder 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<