以太坊的共识算法

您所在的位置:网站首页 比特币跟以太坊的关系怎么样 以太坊的共识算法

以太坊的共识算法

2023-10-27 17:08| 来源: 网络整理| 查看: 265

导言

以太坊是比特币之后的又一大去中心化的区块链应用。作为一个去中心化的应用,以太坊网络能够安全的运行在分布于世界各地的节点之上,依赖于其背后的共识机制。本文讲述以太坊所用到的共识机制。

什么是共识

共识,指的是分布式系统中各个节点的状态达到统一。

就以太坊而言,以太坊的本质是一个状态机,每个节点储存着当前的“世界状态”(world state)(包括账户余额、智能合约数据等),以太坊网络中每一笔交易,比如转账、创建合约,都会改变当前的“世界状态”。如下图所示: image.png (图片摘自Ethereum EVM illastrated)

在实际运行中,为了确定每一笔交易的顺序,避免出现双花问题,以太坊每隔一段时间(10~20秒),会将一批交易打包成一个块,根据块内的交易改变当前的“世界状态”。如下图所示。

image.png (图片摘自Ethereum EVM illastrated)

同时,每个块中还有上一个块的哈希值(parentHash字段),前后链接,以确定交易发生的顺序,以此形成一条“区块链”。

从创始区块开始,每增加一个新区块,都会改变当前的链,进而改变当前的“世界状态”。所以在以太坊的语境中,我们所说的共识,便是就正确的链及“世界状态”达成统一。

以太坊的共识算法

上面说到,以太坊的共识是就正确链达成共识,所以共识算法的工作,就是让网络中的节点拿到正确的链,并且在有恶意节点试图传播错误的链时,共识算法能够拒绝错误链。

接下来,我们说一下目前以太坊正在使用的Proof of Work —— 工作量证明算法

Proof of Work 工作量证明

工作量证明算法的思路是,节点需要付出一定的工作来产出新块,以增加恶意节点生成错误链的难度,并且奖励正确产出新块的节点,以吸引更多诚实节点的加入,增加网络的安全性。

Ethash算法

当以太坊节点想要在现有的链上添加新的块时,需要进行一些计算,为了避免人们开发出专门用于挖矿的机器,造成生产力的浪费,以太坊采用了一种受I/O限制的算法 —— Ethash。以下是算法过程:

1. 准备数据

在进行计算之前,节点首先需要准备两组数据:cache和dataset,用于后面的工作量证明计算。以太坊以30000个节点为一个周期(epoch),从0开始计数:

epoch = Math.floor(block_number / 30000);

每个周期cache和dataset都会发生变化。

首先,节点需要计算一个seed hash值,seed hash长度为256位,初始值为全0,之后下一个周期的seed hash为上一个周期seed hash值的Keccak-256哈希结果。也就是:

const initSeed = Buffer.alloc(32, 0) // 256位0 function getSeed(epoch) { // epoch为周期数 let seed = initSeed; for(let i = 0; i < epoch; i++) { seed = keccak256(seed); } return seed; }

计算出seed hash值后,我们需要计算cache的大小,cache的大小以byte为单位,是常量HASH_BYTE(64)的整数倍。cache大小的上限随着周期线性增长,但是为了避免大小线性增长造成cache数据出现周期性规律,我们以小于当前周期上限的最大HASH_BYTE的质数倍作为该周期的cache大小。具体过程如下:

const INIT_CACHE_SIZE = 2 ** 24; // 初始的cahce大小 const CACHE_SIZE_GROWTH = 2 ** 17; // 每个周期cache的增长量 const HASH_BYTE = 64; function getCacheSize(epoch) { size = INIT_CACHE_SIZE + CACHE_SIZE_GROWTH * epoch; size -= HASH_BYTE; while(!isPrime(size / HASH_BYTE)) { size -= 2 * HASH_BYTE; } return size; }

之后,我们便根据计算出来的cache大小,填充cache的内容。首先是使用seed hash填充(填充过程见代码),填满后,进行三轮RandMemoHash计算,得到最终的cache。具体过程如下:

const xor = require('buffer-xor'); // Buffer上的按位异或 const HASH_BYTE = 64; function makeCache(size, seedHash) { let n = size / HASH_BYTE; // 使用seedHash填充 let cache = [seedHash]; for(let i = 1; i < n; i++) { cache.push(keccak512(cache[i-1])); } // 进行三次RandMemoHash for(let _ = 0; _ < 3; _++) { for(let i = 0; i < n; i++) { v = cache[i].readUInt32LE(0) % n; cache[i] = keccak512(xor(cache[(i - 1 + n) % n], cache[v])); } } return cache; }

得到cache后,我们需要计算第二部分数据 —— dataset。首先,我们要计算dataset的大小。计算方法与计算cache大小类似,只不过初始的参数不同,且单位为MIX_BYTE(128)。如下:

const INIT_DATASET_SIZE = 2 ** 30; // 初始的dataset大小 const DATASET_SIZE_GROWTH = 2 ** 23; // 每个周期dataset的增长量 const MIX_BYTE = 64; function getCacheSize(epoch) { size = INIT_DATASET_SIZE + DATASET_SIZE_GROWTH * epoch; size -= MIX_BYTE; while(!isPrime(size / MIX_BYTE)) { size -= 2 * MIX_BYTE; } return size; }

得到dataset的大小后,我们需要根据cache的内容,计算出dataset的内容。

(代码待补充)

得到cache和dataset的内容后,我们就可以进行工作量证明的计算。

2.工作量证明计算

在区块头中,有两个字段是用来进行工作量证明计算的 —— mixHash和nounce。其中mixHash长度为256位,nounce为64位。我们把除去这两个字段的区块头记为truncatedHeader。

首先介绍一下PoW函数,PoW函数以truncatedHeader、nounce和dataset为输入,经过一些列的哈希运算,输出两个256位的值: mixHash和result。如下:

function PoW(truncatedHeader, nounce, dataset) { // 进行一系列哈希 // ... return { mixHash, result }; }

其中不同的nounce值会产生不同的mixHash和result。

区块头中还有一个字段 —— difficulty,该字段由该区块的区块号、产出时间以及上一区块的difficulty字段计算得来。每个区块的difficulty都会自动调整,控制出块速度。

节点打包一个新的区块头时,是没有mixHash和nounce字段的,也就是truncatedHeader。为了使该区块头上链,节点需要做一下事情:

找到一个nounce值(0 ~ 2^64 - 1),并使用该nounce、truncatedHeader和当前周期的dataset带入PoW函数中,得到mixHash值和result值。 result值需要满足result < (2^256 / difficulty)。 如果result值符合条件,将mixHash值和nounce值写入区块头中,并将区块广播出去,使区块上链。 如果result值不满足,则继续尝试不同的nounce。 如果在计算期间收到了网络中其他节点广播的区块,则验证该区块,如果该区块有效,则接受该区块,重新打包自己的区块,准备加在该区块之后。

以上过程也就是我们常说的“挖矿”,该过程是一个不断尝试nounce并进行哈希运算的过程,需要一定的工作量。

同时,因为每个区块都将上一个区块的哈希值储存在parentHash字段中,所以,如果想要修改链上的其中一个区块,就需要重新计算该区块之后的所有区块的nounce值。这样大大增加了区块修改的难度。

链的选择

从创世区块开始,因为每个节点都能够在上一个区块之后增加新的区块,于是不可避免的,多个节点会往同一高度增加不同的区块,出现分叉(fork),使网络中的区块形成树状结构,如图:

未命名文件 (7).png

那么,网络中的节点如何选择正确的链呢?比特币采用的是最长链策略,选取区块数最多的链,在图中对应的就是0->1->2->3B->4B->5。

而以太坊稍微不同,以太坊首先计算从区块树的根节点(也就是创世节点,Block 0)到每个叶子节点(图中Block 4A、Block 5、Block 4C)路径上区块difficulty的总和,选取difficulty总和最大的那条路径作为正确的链。网络中的节点便会在这条链上增加新的区块。

激励措施

在以太坊中,每产生一个新的区块,就产生2Ether(2021年12月3日)的收益,该收益连同区块上交易的gas费,会进入矿工所指定的地址中。但除此之外,还有一些额外的奖励,称为ommer区块奖励,我们先来了解一下背景。

在比特币中,平均出块时间被控制在10分钟,当其中一个节点找到正确区块后,就向网络中广播该区块,其他节点收到该区块后,就停止当前的计算,重新打包新区块。其中,在网络中广播区块有一定的延迟(别的节点可能需要数十秒才能收到),但是该延迟相较于10分钟的计算时间,可以忽略不计。因此,在计算同一高度的区块时,比特币网络中不会造成太多的重复区块。

但是在以太坊的网络中,平均出块时间很短(10 ~ 20s),很多时候,在计算同一高度的区块时,会出现广播不及时,造成很多多余区块被计算的情况。例如,在计算高度为50000的区块时,节点A找到正确的50000号区块后,向网络中广播。节点B在10s后计算出50000号区块,开始向网络中广播,再过5s才收到节点A计算出来的50000号区块。但是此时网络中的其他节点已经基于A的50000号区块计算出了50001号区块,此时B计算出来的50000号节点便不会被采用了。

因为出块速度快,受网络延迟影响,会出现大量冗余区块,降低矿工的收益,过少的收益会造成矿工的流失,降低网络的安全性。于是以太坊提出了给ommer节点分配收益的方法。

Ommer区块的奖励

Ommer在英语中表示父母的兄弟姐妹,没有性别特指。以太坊为了避免出现性别的争论,没有使用uncle或者aunt,而采用了ommer这个单词。但在以太坊中,ommer区块不仅仅是指与当前区块的父区块有着共同父区块的区块,还包括与当前区块的祖宗六代区块互为兄弟姐妹的区块。

未命名文件 (10).png

如图,以107号区块为例,他的六代祖宗为Block 101 ~ 106,所以六代祖宗的兄弟姐妹区块,也就是101A,104A,104B,106A都属于107号区块的Ommer区块,而100A就不是。

节点在打包107号区块时,会从接收到的ommer区块,将最多两个ommer区块组成列表,一起打包到107号区块中,区块头中的ommerHash字段,就表示ommer列表的哈希值。如果107号区块将ommer区块打包,那么它将额外获得1/32的收益,也就是1/16Ether,作为打包ommer区块的奖励。

所以挖出一个区块的总收益 = 基本收益 + gas费 + 打包ommer区块的奖励。

而被一起打包进107号区块的ommer区块,也会得到一定的Ether作为收益。获得的收益取决于区块的高度和当前区块高度的差值,公式为:

reward = (8 + N_ommer - N_current) / 8 * 2

其中2为当前区块的基本收益(不算打包ommer奖励和gas收益)

比如,106A区块将获得 (8+106 - 107) / 8 * 2 = 14/8 Ether的奖励。

以太坊使用ommer区块奖励,提高了矿工们挖矿的积极性,保证了网络中节点的数量,提升了网络的安全性。

防止恶意节点

对于之前提到的,如果有恶意节点想用自己的链代替网络中的主链,以太坊网络有什么保护措施呢?我们来看下面的图:

未命名文件 (11).png

如图,如果恶意节点打算在101号区块开始分叉,形成一条自己的链,那么,他需要不段地在自己的链上产出新的区块。而某一个节点产出新的区块的概率是和节点算力成正比的,例如,节点A有全网络25%的算力,那么网络中的下一个区块有25%的概率由A产出。

假设恶意节点有25%的算力,那么它产出一个块的概率是25%,两个的概率是0.25^2 = 0.125,连续产生6个的概率是0.25^6 = 0.000244140625,小于0.1%。

所以,只要网络中大部分算力被诚实的节点掌握,我们就能有很大的概率认为,当前网络中的链是正确的链。

更多关于链的正确性的论述,可以参考On Settlement Finality这篇文章

相关资料 Ethereum EVM illastrated On Settlement Finality 以太坊黄皮书


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3