第⼆章⼯作量证明和挖矿
概览
本章节我们将会在我们的玩具版区块链的基础上加⼊⼯作量证明(POW)的⽀持。在的版本中, 任何⼈都都可以在没有任何⼯作量证明的情况下添加⼀个区块到区块链中。 当我们引⼊⼯作量证明机制之后,⼀个节点必须要解开⼀个有相当计算量的拼图(POW Puzzle)之后,才能往区块链上添加⼀个新的区块。⽽去解开该拼图,通常就被称为挖矿。
引⼊⼯作量证明机制之后,我们还可以对⼀个新区块的产出时间作出⼤致的控制。⼤概的做法就是动态的改变拼图的难易程度来达到控制的效果:如果最近的区块产⽣的太快了,那么就将拼图的难度提升,反之,则将拼图的难度降低。
需要点出来的时,本章节中我们还没有引⼊ 交易(Transaction) 这个概念。这就意味着矿⼯挖出⼀个区块后,并不会获得相应的奖励。 ⼀般来说,在加密货币中,如果矿⼯挖到了⼀个区块,是应该获得⼀定量的币作为激励的。
立春朋友圈说说⼯作量证明拼图和难易度
在上⼀个章节的区块链的基础上,我们将会为区块结构加⼊两个新的属性:difficulty和nonce。要弄清楚
头像国旗图标怎么打出这俩货是⼲嘛⽤的,我们必须先对⼯作量证明拼图作⼀些必要的阐述。
⼯作量证明拼图是⼀个什么样的任务呢?其实就是去计算出⼀个满⾜条件的区块哈希。怎么才算满⾜条件呢?如果这个计算出来的哈希的前⾯的0的数⽬满⾜指定的个数,那么就算满⾜条件。那么这个个数⼜是谁指定的呢?对,就是上⾯的这个difficulty指定的。如果difficulty 指定说哈希前⾯要有4个0,但你计算出来的哈希前⾯只有3个0,那么这个哈希就不是个有效的哈希。
下图展⽰的就是不同难易程度的情况下,哈希是否有效:
以下代码⽤来检查指定difficulty下哈希是否有效:
const hashMatchesDifficulty = (hash: string, difficulty: number): boolean => {
const hashInBinary: string = hexToBinary(hash);
const requiredPrefix: string = '0'.repeat(difficulty);
return hashInBinary.startsWith(requiredPrefix);
};
区块的哈希是通过对区块的内容算sha256来获得的,通过相同的区块内容做哈希,我们是⽆法算出符合指定difficulty的哈希, 因为内容⼀直没有变,哈希也就⼀直不变, 这就是为什么我们引⼊了nonce这个属性到区块中,我们可以控制nonce的改变来获得不同的哈希值。只要我们的内容有任何⼀点点的改变,算出来的哈希就肯定是不⼀样的。⼀旦相应的nonce修改后让我们获得到指定difficulty的哈希,我们挖矿也就成功了!
既然加⼊了difficulty和nonce到区块,我们还是先看看区块结构现在长什么样吧:
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;
< = nonce;
}
}
当然,我们的创世区块是硬编码的,记得把它也给更新成相应的结构哦。
挖矿
如上所述, 为了解开我们的⼯作量证明拼图这个任务,我们需要不停的修改区块中的nonce然后计算修改后的哈希,直到算出满⾜条件的哈希。这个满⾜条件的哈希什么时候才会跑出来则完全是个随机的事情。所以我们要做的就是给nonce⼀个初始值,然后不停的循环,修改,计算哈希,直到满⾜条件的⼼仪的它的出现。
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++;
}
};
flash场景⼀旦满⾜条件的区块的哈希给到了,挖矿成功!然后我们就需要将该区块⼴播到⽹络上,让其他节点接受我们的区块,并更新最新的账本。这个和第⼀章节的情况并⽆⼆致。
难易度共识
我们现在已经拥有出和验证满⾜指定难易度的区块的哈希的⼿段了,但是,这个难易程度是如何决定的呢?必须要有⼀个⽅法让全⽹各个节点⼀致认同这个决定。不然我的难易度是要挖半天才能出来,
别⼈的是⼏毫秒就出来,那这个区块链⽹络就有问题。
所以,这⾥必须要有⼀个⽅法来让所有的节点都⼀致认同当前挖矿的难易度。为了做到这⼀点,我们⾸先引⼊⼀些⽤于计算当前难易度的规则。
先定义以下的⼀些常量:
BLOCK_GENERATION_INTERVAL: 定义 ⼀个区块产出的 频率(⽐特币中该值是设置成10分钟的, 即每10分钟产出⼀个区块) DIFFICULTY_ADJUSTMENT_INTERVAL: 定义 修改难易度以适应⽹络不断增加或者降低的⽹络算⼒(hashRate, 即每秒能计算的哈希的数量)的 频率(⽐特币中是设置成2016个区块, 即每2016个区块,然后根据这些区块⽣成的耗时,做⼀次难易度调整)
这⾥我们将会把区块产出间隔设置成10秒,难易度调整间隔设置成10个区块。这些常量是不会随着时间⽽改变的,所以我们将其硬编码如下:
const BLOCK_GENERATION_INTERVAL: number = 10;
// in blocks
const DIFFICULTY_ADJUSTMENT_INTERVAL: number = 10;
有了这些规则,我们就可以在我们的⽹络上达成难易度的⼀致性了。每产出10个区块之后,我们就去检查⽣成所有这10个区块所消耗的时间,然后和预期时间进⾏对⽐,然后对难易度进⾏动态的调整。
这⾥的预期时间是这样指定的:BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL。 该预期时间代表了当前⽹络的算⼒刚刚好和当前的难易度吻合。
如果耗时超过或者不⾜预期时间的两倍,那么我们就会将difficulty进⾏对应的减1或者加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;
}
};
时间戳校验
第⼀章节中的区块链版本中,区块结构中的时间戳属性是没有任何意义的,因为我们不会对其作任何校验的⼯作,也就是说,我们其实可以给该时间戳赋以任何内容。 但现在情况变了,因为我们这⾥引⼊了难易度的动态调整,⽽该调整是基于前10个区块产出的耗时总长度的。所以时间戳我们就不能像之前⼀样随意赋值了。
既然时间戳的⼤⼩会影响区块产出难易度的调整,所以别有⽤⼼的⼈就会考虑去恶意设置⼀个错误的时间戳来尝试操纵我们的难易度,以实现对我们的区块链⽹络进⾏攻击。为了规避这种风险,我们需要引⼊以下的规则:
新区块时间戳⽐当前时间最晚不晚过1分钟,那么我们认为该区块有效。
新区块时间戳⽐前⼀区块的时间戳最早不早过1分钟,那么我们任务该区块在区块链中是有效的。
const isValidTimestamp = (newBlock: Block, previousBlock: Block): boolean => {
return ( newBlock.timestamp - 60 < getCurrentTimestamp() )
&& ( previousBlock.timestamp - 60 < newBlock.timestamp );
};
天地会珠海分舵注:这⾥为什么要有⼀分钟的缓冲呢?估计是为了既考虑⼀定程度的容错,也减缓了时间戳恶意修改的攻击。如果还有其他原因的,请不吝指教。
累积难易度
还记得第⼀章节的区块链的版本中,我们定的规则是:⼀旦发⽣冲突,我们总是选择最长的区块链作为有效的链进⾏更新,以⽰对更多区块⽣产者的努⼒的肯定。因为我们现在引⼊了挖矿的难易度,所以我们不应该再这样⼦做了,我们更应该对投⼊资源更多计算⽽出的那条链进⾏肯定。也就是说,现在正确的链,不是最长的那条链,⽽是累积难易度最⼤的那条链。什么意思呢?换个说法就是,正确的链,应该是那条消耗了最多计算资源(⽹络算⼒*耗时)⽽产⽣出来的链。
那么怎么算出⼀条链的累积难易度呢?我们⾸先对区块链中每个区块的difficulty进⾏2^difficulty计算,然后将每个区块的计算结果加起来,最终就是这条链的累积难易度。台风级别
这⾥为什么我们⽤2^difficulty来计算呢?因为difficulty代表了哈希的⼆进制表⽰中前⾯的0的位数。试想下,⼀个difficulty是11的块和⼀个difficulty是5的块相⽐,总的来说,我们需要多计算2^(11-5) = 2^6 次哈希才能获得想要的结果。因为每⼀位在⼆进制中都有0和1两种变化,他们之间相差6个位,固有2^6个变化。
下图中,虽然链A⽐链B长,但因为链B的累积难易度⽐链A⼤,所以链B才是正确的链.
同时我们还要注意的是,我们累积难易度只和区块的difficulty属性有关系,和区块的真实哈希即其前⾯的位数是没有任何关系的。 拿⼀个difficulty为4的区块来说,假如它的哈希是(该哈希同时也满⾜difficulty为5和6的情况),我们计算累积难易度是还是会以2^4来算,⽽不是2^5或者2^6, 即使它前⾯有6个0.
这种根据难易度选择正确的链的策略也叫做中本聪共识(Nakamoto consensus), 这也是中本聪的⽐特币中最重要的⼀个发明之⼀。⼀旦账本出现分叉,矿⼯们就必须选择⼀条链来投⼊资源继续进⾏挖矿,因为这个关系到矿⼯挖矿后的激励,所以选择正确的链必须全⽹达成共识。
验证测试
安装运⾏
npm install
npm run node1
npm run node2
挖矿
我们可以尝试通过curl或者postman调⽤/mineBlock这个api接⼝来进⾏挖矿,并查看对应的输出.
难易度调整
在通过不同的时间间隔调⽤完10次/mineBlock这个接⼝之后,难易度就会相应的进⾏变化
选择最⼤累积难易度的链
这个真实情况不好模拟出来。 但是我们可以去验证对应的算法。我们可以先开⼀个节点,创建3个以上的区块之后,再开第⼆个节点。这时节点2就会去获取节点1的所有区块,然后运⾏对应的逻辑去选择最⼤累积难易度的链。
⼩结
⼯作量证明拼图的⼀个重要特点就是难以解开但易于验证。所以不断调整nonce以算出⼀个满⾜⼀定难度的SHA256哈希,然后简单的验证前⾯⼏位是否是0,往往就是这种问题最简单的解决⽅案。
有了⼯作量证明机制后,节点现在就需要挖矿,也就是解决⼯作量证明拼图,才能往区块链中新增加⼀个区块了。在下⼀章节中,我们将会为我们的区块链引⼊交易(Transaction)功能。
本章节代码请查看
本⽂由天地会珠海分舵编译,转载需授权,喜欢点个赞,吐槽请评论,如能给Github上的项⽬给个星,将不胜感激.
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论