在本系列前两篇文章中, 我们向大家展示了如何通过精炼的 Java 代码实现一个简单的区块链包括生成块, 验证块数据, 广播通信等等, 这一篇让我们聚焦在如何实现 PoW 算法
大家都无不惊呼比特币以太坊及其他加密电子货币的持续狂热, 特别是对于刚接触这个领域的新手, 不断得听到张三李四通过 GPU 挖矿而聚集价值数万乃至数百万加密电子货币那么挖矿到底是什么? 它是如何工作的? 相信对于程序员来说, 没有什么比自己动手实践一遍挖矿算法更好的学习办法了
在这篇文章中, 让我们一起逐个解读每一个问题, 并最终编写出自己的挖矿算法这个算法称为工作证明算法(Proof-of-Work), 它是比特币和以太坊这两种最流行的加密货币的基础
什么是挖矿?
加密电子货币因为稀缺才具有价值以现在的比特币为例, 如果任何人任何时候都可以随意制造比特币, 那么作为电子货币它会变得毫无价值比特币通过算法来控制产出的速率并在大约 122 年内达到最大量这种随着时间推移缓慢稳定并逐步产出的方式有效避免了通货膨胀
比特币的产出是通过给予获胜矿工奖励来实现, 为了获取比特币奖励矿工之间会进行竞争这个过程之所以被称为挖矿, 是因为它类似于 Gold Rush 时每个黄金矿工通过辛苦劳作并最终 (希望) 找到一点黄金
挖矿是如何工作的?
如果 Google 一下这个问题, 你会得到大量的结果简单来说, 挖矿就是解决一个数学难题的过程我们先来了解一些密码学和哈希算法的知识
密码学简要介绍
单向加密以人类可读的文本 (明文) 作为输入, 比如 Hello world 这个字符串, 再通过一个数学函数产生出难以辨认的输出(密文) 这类函数或算法的性质和复杂性各不相同 算法越复杂, 逆向工程就越困难
以流行的 SHA-256 算法为例 通过这个网站 [5] 可以让你计算任意给定输入的输出, 也就是 SHA-256 哈希值比如让我们输入 Hello world, 看看得到了什么:
通过不断尝试计算 Hello world 的哈希值你会发现每次的结果都完全相同 这个过程称为幂等性
加密算法一个最基本的特性是, 非常难以通过反向工程来求解输入, 但是非常容易验证输出比如上面的例子, 你可以很容易验证给定输入 Hello world 的 SHA-256 哈希值是否正确, 但很难通过给定的哈希值判断它的输入是什么这就是为什么将这种类型的算法称为单向加密
比特币使用 Double SHA-256, 它将 SHA-256 求得的哈希值作为输入再次计算 SHA-256 哈希值 为了简化, 我们只使用一次 SHA-256
挖矿
回到加密电子货币中, 比特币就是通过让参与者利用这样的加密算法求解出符合特定条件的哈希值来实现挖矿过程具体来说, 比特币要求参与者通过 double SHA-256 算法计算出前导 0 超过若干位的哈希值, 第一个求解出来的参与者就是获胜的矿工
比如, 我们求 886 这个字符串的 SHA-256 哈希值:
可以看到, 是一个前导 0 为 3 位的哈希值(前三位是 0)
回忆我们前面说到的单向加密的特点:
任何人都可以很容易地验证 886 是否产生 3 位前导 0 的哈希值但为了找到这样一个能产生 3 位前导 0 的输入(就是这里的 886), 我们做了大量繁琐的计算工作: 从一个很大的数字和字母集合中逐个计算它们的哈希值并判断是否满足上述条件如果我是第一个找到 886 的人, 那其他人通过验证就能判断我做了这样大量繁琐的工作在比特币以太坊中这样的过程就称为工作证明算法
如果我运气非常好, 第一次尝试就找到了一个符合条件的 (输入) 值呢? 这是非常不可能的, 你可以试试随意输入一些字母和数字
比特币中实际的算法和约束要比上说要求复杂, 当然也更难 (要求更多位的前导 0) 同时它也可以动态调整难度, 目标是确保每隔 10 分钟产出一次比特币, 不管参与挖矿的人多还是少
差不多可以动手了
了解了足够的背景知识, 接着我们就用 Java 语言来编码实践下工作量证明 (Proof-of-Work) 算法建议你阅读之前的系列文章, 因为下面工作证明算法部分会涉及之前的代码
Proof-of-work
创建新块并加入到链上之前需要完成工作量证明过程我们先写一个简单的函数来检查给定的哈希值是否满足要求
哈希值必须具有给定位的前导 0
前导 0 的位数是由难度 (difficulty) 决定的
可以动态调整难度 (difficulty) 来确保 Proof-of-Work 更难解
下面就是 isHashValid 这个函数:
- /**
- * 校验 HASH 的合法性
- *
- * @param hash
- * @param difficulty
- * @return
- */
- public static boolean isHashValid(String hash, int difficulty) {
- String prefix = repeat("0", difficulty);
- return hash.startsWith(prefix);
- }
- private static String repeat(String str, int repeat) {
- final StringBuilder buf = new StringBuilder();
- for (int i = 0; i < repeat; i++) {
- buf.append(str);
- }
- return buf.toString();
- }
我们定义 prefix 变量, 它代表前导 0, 接着检查哈希值是否具有满足条件的前导 0, 然后返回 True 或 False
我们修改之前生成块的 generateBlock 函数:
- public static Block generateBlock(Block oldBlock, int vac) {
- Block newBlock = new Block();
- newBlock.setIndex(oldBlock.getIndex() + 1);
- newBlock.setTimestamp(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
- newBlock.setVac(vac);
- newBlock.setPrevHash(oldBlock.getHash());
- newBlock.setDifficulty(difficulty);
- /*
- * 这里的 for 循环很重要: 获得 i 的十六进制表示 , 将 Nonce 设置为这个值, 并传入 calculateHash 计算哈希值
- * 之后通过上面的 isHashValid 函数判断是否满足难度要求, 如果不满足就重复尝试 这个计算过程会一直持续, 直到求得了满足要求的
- * Nonce 值, 之后通过 handleWriteBlock 函数将新块加入到链上
- */
- for (int i = 0;; i++) {
- String hex = String.format("%x", i);
- newBlock.setNonce(hex);
- if (!isHashValid(calculateHash(newBlock), newBlock.getDifficulty())) {
- System.out.printf("%s do more work!\n", calculateHash(newBlock));
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- LOGGER.error("error:", e);
- }
- continue;
- } else {
- System.out.printf("%s work done!\n", calculateHash(newBlock));
- newBlock.setHash(calculateHash(newBlock));
- break;
- }
- }
- return newBlock;
- }
篇幅有限, 我已经将完整代码发布在 Github 上, 可以从这里获得
跑起来看看
启动程序, 在浏览器中访问
http://localhost:8080
接着通过 RestClient 来发送一个包含 vac 数据的 POST 请求我们观察命令行窗口, 不断得计算哈希值, 如果不满足难度要求就继续重试, 直到找到满足要求的哈希值及 Nonce
可以看到最后一个哈希值满足我们设定的难度要求 (1 位前导 0) 我们再来刷新下浏览器:
可以看到第二个块创建成功并加到链上了, 其中 Nonce 就是通过 Proof-of-Work 计算出来满足难度要求的值
下一步
到这里要先祝贺你, 上面的内容很有价值尽管我们的示例中使用了非常低的难度, 但本质上, 工作证明算法就是比特币以太坊等区块链的重要组成
对于下一步应该深入区块链的哪个方向, 我们推荐可以学习如何通过 IPFS 存取大文件并与区块链打通[IPFS]
此外相比 Proof-of-Work,Proof-of-Stake 算法正越来越受到关注和青睐, 你也可以学习如何将本文的 PoW 算法改为实现 PoS 算法
来源: http://www.bubuko.com/infodetail-2521481.html