本章,我们将为玩具区块链版本实施一个简单的权益证明方案。在第一章版本中,任何人都可以在不增加成本的情况下将区块添加到区块链中。通过权益证明,我们可以在块添加到区块链之前引入需要解决的计算难题。视图解决这个难题通常被称为“挖矿”。
通过权益证明验证,我们还可以控制(大约)区块链引入块的频率。这是通过改变难题的难度来完成的。如果区块被挖掘出来过于频繁,难题的难度就会增加,反之亦然。
应指出,本章中还没有介绍交易。这意味着矿工实际上没有激励来生产块。通常在加密货币中,矿工会因为寻找到块而获得奖励,但在我们的区块链中情况并非如此。
本章将要实现的文章代码请戳这里
我们将在块结构中新增两个属性:difficulty
(译者注:难度系数) 和 nonce
(译者注:随机数)。为了便于理解其含义,必须先介绍权益证明难题。
权益证明难题是找到一个拥有特定数量的0作为前缀的块hash。difficulty
属性定义了块hash必须有多少个前缀0,以使块有效。前缀的0通过块hash的二进制格式进行检查。
检查hash在difficulty方面是否正确的代码如下:
const hashMatchesDifficulty = (hash: string, difficulty: number): boolean => {
const hashInBinary: string = hexToBinary(hash);
const requiredPrefix: string = '0'.repeat(difficulty);
return hashInBinary.startsWith(requiredPrefix);
};
为了找到满足难度系数的hash值,必须能够为相同内容的块计算不同的hash值。这是通过修改nonce
参数来完成的。由于SHA256是一个hash函数,每当块中的任何内容发生变化时,hash会完全不同。“挖矿”基本上只是尝试不同的随机数,直到块hash值符合难度系数的要求。
增加了difficulty
和nonce
的块结构如下:
class Block {
public index: number;
public hash: string;
public previousHash: string;
public timestamp: number;
public data: string;
public difficulty: number;
public nonce: number;
constructor(index: number, hash: string, previousHash: string,
timestamp: number, data: string, difficulty: number, nonce: number) {
this.index = index;
this.previousHash = previousHash;
this.timestamp = timestamp;
this.data = data;
this.hash = hash;
this.difficulty = difficulty;
this.nonce = nonce;
}
}
我们还须记得更新起始块!
如上所述,为了找到一个有效的块hash,必须增加nonce,直到得到一个有效的hash。找到满意的hash完全是一个随机过程。我们必须通过足够的随机循环,直到找到满意的hash为止:
const findBlock = (index: number, previousHash: string, timestamp: number, data: string, difficulty: number): Block => {
let nonce = 0;
while (true) {
const hash: string = calculateHash(index, previousHash, timestamp, data, difficulty, nonce);
if (hashMatchesDifficulty(hash, difficulty)) {
return new Block(index, hash, previousHash, timestamp, data, difficulty, nonce);
}
nonce++;
}
};
当找到该块时,就像第一章中所述那样将其广播到网络中。
现在我们已经有办法查找并验证hash值,但是如何确定难度系数呢?节点必须有一种方式来同意目前的困难。为此,我们介绍一些用来计算当前网络难度的新规则。
我们为网络定义以下新的常量:
BLOCK_GENERATION_INTERVAL
,定义应该找到块的频率。(在比特币中这个值是10分钟)DIFFICULTY_ADJUSTMENT_INTERVAL
,定义了难度应该随网络hash率的增加或减少而调整的频率。(在比特币中,这个值是2016个块)
我们将块生成间隔设置为10秒,难度调整为10个块。这些常量不会随时间变化,且它们是硬编码的。
// in seconds
const BLOCK_GENERATION_INTERVAL: number = 10;
// in blocks
const DIFFICULTY_ADJUSTMENT_INTERVAL: number = 10;
现在我们有办法就区块的困难系数达成一致。对于每10个生成的块,我们检查生成这些块的时间是大于还是小于预期时间。预期时间计算如下:BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL
。预期时间表示hash率与当前困难度完全匹配的情况。
如果所花费的时间比预期难度至少大两倍或者更小,可以将难度系数提高或者降低1,。难度调整由以下代码处理:
const getDifficulty = (aBlockchain: Block[]): number => {
const latestBlock: Block = aBlockchain[blockchain.length - 1];
if (latestBlock.index % DIFFICULTY_ADJUSTMENT_INTERVAL === 0 && latestBlock.index !== 0) {
return getAdjustedDifficulty(latestBlock, aBlockchain);
} else {
return latestBlock.difficulty;
}
};
const getAdjustedDifficulty = (latestBlock: Block, aBlockchain: Block[]) => {
const prevAdjustmentBlock: Block = aBlockchain[blockchain.length - DIFFICULTY_ADJUSTMENT_INTERVAL];
const timeExpected: number = BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL;
const timeTaken: number = latestBlock.timestamp - prevAdjustmentBlock.timestamp;
if (timeTaken < timeExpected / 2) {
return prevAdjustmentBlock.difficulty + 1;
} else if (timeTaken > timeExpected * 2) {
return prevAdjustmentBlock.difficulty - 1;
} else {
return prevAdjustmentBlock.difficulty;
}
};
第一章中,时间戳没有任何验证作用。事实上,它可能是客户端决定生成的任何东西。现在情况发生了变化,因为引入了难度调整timeTaken变量(在前面的代码段中),它是根据块的时间戳计算而来的。
为了减轻引入错误时间戳的攻击以便处理难度系数,引入以下规则:
- 如果时间戳在前一个块的时间戳之前最多一分钟内,则区块链中的块是有效的。
- 如果时间戳在感知到的时间起未来最多一分钟内,则块是有效的。
const isValidTimestamp = (newBlock: Block, previousBlock: Block): boolean => {
return ( previousBlock.timestamp - 60 < newBlock.timestamp )
&& newBlock.timestamp - 60 < getCurrentTimestamp();
};
在上一章的区块链版本中,始终选择“最长”区块链作为有效区块链。有了难度系数后方案必须要调整。现在正确的链条不是“最长”的,而是具有最大累积库困难度的链条。换言之,正确的链是一条需要大部分资源(= hashRate * time)才能生成的链条。
为了获得链条的累积困难度,需要为每个块执行2的difficulty次方次计算,并将所有块的计算次数累加。我们必须使用2的difficulty次方来表示计算次数,因为我们选择了用难度系数来表示二进制hash前缀的0的个数。例如,如果我们比较5和11这两个难度系数,后者需要额外增加2^(11-5) = 2^6的工作量才能找到后一块块。
以下例子中,虽然链条A拥有更长的链条,但链条B才是正确的链条。
计算累积难度时,只有块的难度系数有用,而不是实际的hash(假设hash是有效的)。例如,如果难度系数为4,hash为:000000a34c……(也满足6的难度),则在计算累积难度时仅考虑为4的难度系数。
这个属性也被成为“Nakamoto共识”,它是Satoshi在发明比特币时最重要的发明之一。在分叉的情况下,矿工必须选择他们决定将当前的资源(hashRate)放置哪个链条上。由于矿工的兴趣在于生产将包含在区块链中的此类区块,因此矿工们获得激励,最终选择相同的链条。
权益证明难题必须具备的一个重要特征是它很难解决,但易于验证。寻找特定的SHA256 hash值是这类问题的一个很好和简单的例子。
我们实现了难度方面,但节点现在必须“挖矿”,以便链条可以添加新块。在下一章中,我们将实现交易。
本章完整代码可以在这里找到