列旭松
来自:Linux内核那些事(微信号:like_linux)
作者:列旭松,唯品会资深工程师,曾任职于YY语音,熟识PHP、C语言和Go语言。10年PHP开发经验,对PHP底层实现原理有较深理解。热衷于开源事业,开源过多个PHP相关的扩展,流行的PHP源码加密扩展(PHP-Beast)作者。另外,本人对分布式缓存系统(如Redis、Memcached)有较大的兴趣,喜欢钻研底层实现原理,《 PHP 核心技术与最佳实践》一书的作者。
1、前言
上一篇文章我们介绍了区块链的最基本数据结构-区块,而且还构建了一个最原始的区块链。但是现在我们很容易就可以向区块链中添加区块,这样有可能导致大量的区块在同时添加到区块链中,从而导致广播风暴。而且在分布式环境中,如果并发量太大会导致很多问题,例如拜占庭问题。
为了解决这个问题,比特币使用了工作量证明机制。
2、工作量证明
由于现在添加一个区块的成本很低,所以必须找到一个增加添加区块成本的方法,而在比特币中使用的是工作量证明,我们也效仿比特币使用工作量证明。
工作量证明(POW,Proof-of-Work)是一个用于阻止拒绝服务攻击和类似垃圾邮件等服务错误问题的协议,它在 1993 年被 Cynthia Dwork 和 Moni Naor 提出。
那么什么是工作量证明呢?其实很简单,就是找到一个符合某一规定的Hash值。例如我们规定区块的Hash值的前 20 个位必须为 0,要符合这样的区块才能添加到区块链中,那么工作量证明就是要找到符合这样规定的Hash值。
由于找到这样的Hash值是非常困难的,所以会给予找到合适Hash值的人相应奖励(比特币),找到符合规定Hash值的过程被称为“挖矿”,而挖矿的机器被称为“矿工”。
3、Hash值计算
如前面所说,我们需要找到一个符合规定的Hash值才能将区块添加到区块链中,而在我们的前一篇文章中计算区块Hash值的方法是直接序列化区块,然后使用SHA-256来计算其Hash值。那么有个问题是,区块的内容是不变的,所以计算出来的Hash值也是固定的,那么有什么办法来改变区块的Hash值呢?这里我们使用Hashcash 算法,步骤如下:
1. 取一些公开的数据(比如区块头)。
2. 给这个公开数据添加一个计数器。计数器默认从 0 开始。
3. 将数据和计数器组合到一起,获得一个Hash值。
4. 检查Hash值是否符合规定的条件:
1) 如果符合条件,结束
2) 如果不符合,增加计数器,重复步骤 3-4
可以看出这个是一个暴力计算的过程:改变计数器,计算新的Hash值,检查是否符合条件,直到找到符合条件的Hash值。
在 Hashcash 算法中,它的要求是“一个Hash值的前 20 位必须是 0”。条件越苛刻,找到符合条件的Hash值就越难。下图详细说明了这个过程:
在上图中,129022是计数器的值,而符合条件的Hash值是前 16 个位为 0 (上图中表现为Hash值的前4个字符为0)。
4、实现
在上一篇文章中的Block对象的实现中,添加一个findBlockHash()的方法用于找到符合条件的Hash值:
class Block
{
...
public $nonce;
private function prepareData($nonce)
{
return json_encode([
$this->prevHash,
$this->timeStamp,
$this->data,
$nonce,
]);
}
public function findBlockHash()
{
$found = false;
$condition = '0000'; // Hash值前N个字符必须等于$condition
$condlength = strlen($condition);
printf("Mining the block containing "%s" ", $this->data);
for ($nonce = 0; $nonce < PHP_INT_MAX; $nonce++) {
$data = $this->prepareData($nonce);
$hash = hash('sha256', $data);
printf(" %d: %s", $nonce, $hash);
if (substr($hash, 0, $condlength) === $condition) {
$found = true;
break;
}
}
print(" ");
if ($found) {
$this->nonce = $nonce;
$this->hash = $hash;
}
return $found;
}
}
在上面的代码中,我们为Block类添加了一个nonce的成员变量,用于记录计数器。而在findBlockHash()函数中,我们不断增加计数器的值,计算出Hash值,然后比较Hash值是否符合条件(前N个字符是否等于变量$condition)。如果找到合适的Hash值就退出循环,否则增加计数器的值计算下一个Hash值。而prepareData()方法用于序列化要计算Hash值的数据。
最后,我们在Block类的构造函数中调用findBlockHash()方法:
class Block
{
...
public function __construct($prevHash, $data)
{
$this->prevHash = $prevHash;
$this->timeStamp = time();
$this->data = $data;
$this->findBlockHash();
}
...
}
现在我们来运行一下代码看看效果:
从运行结果可以看到,算出来的Hash值都符合我们规定的条件。当然,你可以修改更苛刻的条件来增加挖矿的难度。
最后还有一件事需要做,就是验证区块是否合法:
class Block
{
...
public function validate()
{
$condition = '0000';
$condlength = strlen($condition);
$data = $this->prepareData($this->nonce);
return substr(hash('sha256', $data), 0, $condlength) === $condition;
}
}
验证的方法很简单,就是计算出区块的Hash值,然后比较Hash值是否符合条件。然后我们在打印区块链的时候验证区块是否合法:
include('blockchain.php');
$bc = new Blockchain();
$bc->addBlock('This is block1');
$bc->addBlock('This is block2');
foreach ($bc->blocks as $block) {
...
printf("PoW: %s ", $block->validate() ? 'true' : 'false');
...
}
运行代码查看结果:
结果符合我们的预期,代码在:https://github.com/liexusong/blockchain-php/tree/v2.0