一、介绍
1.1 什么是PoW
POW,proof of work,即工作量证明,是bitcoin所采用的共识算法。简单来说,pow就是一个证明,由矿工使用算力进行计算,竞争记账权,获得记账权的矿工将获得奖励和记录账本的权力。
其过程大致如下:
-
全网矿工会监听全网的数据记录
-
收到数据后,矿工验证交易并构建区块信息,包括区块头和区块体
-
努力进行hash运算(求X)
-
找到hash的矿工向全网公告自己产生的新区块
1.2 Ethash共识算法
Engine是以太坊封定义的一个接口,它的功能可以分为3类:
- 验证区块类,主要用在将区块加入到区块链前,对区块进行共识验证。
- 产生区块类,主要用在挖矿时。
- 辅助类。
验证区块类 - Prepare:初始化区块头信息,不同的共识算法初始化不同。使用场景是,worker创建work的时候调用。
- Finalize:根据数据生成“基本定型”的区块,但区块头中还缺少部分数据。使用场景是
1)模拟区块链的时候,被GenerateChain调用,用来生成区块链。
2)交易状态管理时,被StateProcessor.Process调用用来执行交易。
3)worker创建work的时候调用。 - Seal:根据传入的块,进行的是挖矿工作,使用挖矿的结果,修改区块头,然后生成新的区块。使用场景是,被agent.mine调用。
验证区块类
- VerifyHeader:验证区块头。使用在fetcher中,当fetcher要插入区块的时候,需要先对区块头进行校验。
- VerifyHeaders:验证一批区块头。有2种使用场景
1)区块链中,insertChain当把一批区块插入到区块链这个链条的时候,需要进行检查;
2)LightChain中,把一批区块头插入到本地链。 - VerifyUncles:验证区块中的叔块。insertChain当区块插入区块链的时候,需要对叔块进行验证,调用在VerifyHeaders之后。
- VerifySeal:针对Seal函数做的功能进行验证。验证Seal()所修改的区块头中的数据。对外的使用场景是,把Work发送给远端Agent的时候调用。对内的使用场景是,验证区块头的时候会被调用。
辅助类
- APIs:生成以太坊共识相关的API。在以太坊启动RPC服务时,生成API。
- Author:读取区块头中的coinbase。被ethstats使用,ethstats是以太坊状态管理服务,当报告数据的时候,需要获取区块的Author信息。
最后关注一下蓝色的线条,它们代表insertChain所调用的范围,先关的有VerifyHeaders、VerifyUncles、Finalize,涉及到块头的验证、叔块的验证,以及执行区块中的交易,一个区块加入到区块链中,不仅要验证,还要执行各种交易,改变各种状态,所有节点执行确定性的行为之后,达成一致性。
FAQ
Q:谁实现engine
A:以太坊中的Ethash和Clique实现了engine,Ethash是基于PoW的共识,Clique是基于PoA的共识。
Q:为什么insertChain没有调用VerifySeal?
A:因为Seal()修改的是header中的部分数据,在验证区块头的时候,会被调用。只是调用流程在Ethash和Clique中的实现略有不同,后续讲解。
以太坊码源
共识算法族对外暴露的是Engine接口,其有两种实现体,分别是基于运算能力的Ethash算法
和基于“同行”认证的的Clique算法
。
在Engine接口的声明函数中:
- VerifyHeader(),VerifyHeaders(),VerifyUncles()用来验证区块相应数据成员是否合理合规,可否放入区块;
- Prepare()函数往往在Header创建时调用,用来对Header.Difficulty等属性赋值;
- Finalize()函数在区块区块的数据成员都已具备时被调用,比如叔区块(uncles)已经具备,全部交易Transactions已经执行完毕,全部收据(Receipt[])也已收集完毕,此时Finalize()会最终生成Root,TxHash,UncleHash,ReceiptHash等成员。
而Seal()和VerifySeal()是Engine接口所有函数中最重要的。
- Seal()函数可对一个调用过Finalize()的区块进行授权或封印,并将封印过程产生的一些值赋予区块中剩余尚未赋值的成员(Header.Nonce,Header.MixDigest)。Seal()成功时返回的区块全部成员齐整,可视为一个正常区块,可被广播到整个网络中,也可以被插入区块链等。所以,对于挖掘一个新区块来说,所有相关代码里Engine.Seal()是其中最重要,也是最复杂的一步。
- VerifySeal()函数基于跟Seal()完全一样的算法原理,通过验证区块的某些属性(Header.Nonce,Header.MixDigest等)是否正确,来确定该区块是否已经经过Seal操作。
在两种共识算法的实现中,Ethash是产品环境下以太坊真正使用的共识算法,Clique主要针对以太坊的测试网络运作,两种共识算法的差异,主要体现在Seal()的实现上。
1.2.1 Ethash本质
Ethash算法
又被称为Proof-of-Work(PoW),是基于运算能力的授权/封印过程。Ethash实现的Seal()函数,其基本原理可简单表示成以下公式:
Rand(hash, nonce) ≤ MaxValue / Difficulty
参数解释
- hash:去除区块头中Nonce、MixDigest生成的哈希值,见HashNoNonce()。
- nonce:待寻找的符合条件的随机数。
- MaxValue:固定值2^256,生成的哈希值的最大取值。
- Difficulty:挖矿难度。
- Rand():使用hash和nonce生成一个哈希值,这其中包含了很多哈希运算。
以上参数中,在得到区块头的hash之后,只有nonce是未知的。公式的含义是,使用hash和nonce生成的哈希值必须落在合法的区间
利用下图介绍一下,Rand()函数结果取值范围是[0, MaxValue],但只有计算出的哈希值在[0, MaxValue / Difficulty]内,才是符合条件的哈希值,进而该Nonce才是符合条件的,否则只能再去寻找下一个Nonce。
以太坊可以通过调整Difficulty来调节当前挖矿的难度,Difficulty越大,挖矿的难度越大。当Difficulty越大时, MaxValue / Difficulty越小,合法的哈希值范围越小,造成挖矿难度增加。
哈希值满足条件的概率是 p = (MaxValue / Difficulty) / MaxValue = 1 / Difficulty,矿工需要进行1 / p = Difficulty次的判断,才有可能找到一个符合条件的Nonce,当前以太坊难度为3241847139727150
为什么PoW需要做那么多的运算,而不是通过公式反推,计算出满足条件的结果(Nonce)?
PoW可以表示为许多数学公式的合集,每次运算的入参:前一个区块头的哈希,当前高度的DataSet,目标值Nonce,这些数学公式都是哈希函数,哈希函数的特性就是不可逆性,不能通过摘要获得输入数据。虽然,前一个区块头的哈希和当前高度的DataSet是固定的,但由于哈希函数的不可逆性,依然无法倒推出Nonce,只能随机的产生Nonce,或累加Nonce,并不断的重试,直到找到符合条件的Nonce。
1.2.2 Ethash主要思想
Ethash的主要思想是,开启多个线程去寻找符合条件的Nonce,给每个线程分配一个随机数,作为本线程的Nonce的初始值,然后每个线程判断当前的Nonce是否符合上面的公式,如果不符合,则把Nonce加1,再次进行判断,这样不定的迭代下去,直到找到一个符合条件的Nonce,或者挖矿被叫停。
接下来介绍几个主要函数的实现,它们是:
- 入口Seal函数。
- 函数mine函数。
- 需要的数据cache和dataset。
- Rand()函数的实现hashimotoFull和hashimoto。
入口Seal()
Seal是引擎的入口函数,它是管理岗位,负责管理打包区块的线程。它发起多个线程执行Ethash.mine进行并行挖矿,当要更新或者停止的时候,重新启动或停止这些线程。
函数mine()
mine函数负责挖矿。Seal在启动每一个mine的时候,给它分配了一个seed,mine会把它作为Nonce的初始值,然后生成本高度使用的dataset,然后把dataset, hash, nonce传递给hashimotoFull函数,这个函数可以认为是原理介绍中的Rand随机函数,他会生成哈希值Result,当Result <= Target的时候,说明哈希值落在符合条件的区间了,mine找到了符合条件的Nonce,使用Digest和nonce组成新的区块后,发送给Seal,否则验证下一个Nonce是否是符合条件的
需要的数据cache和dataset
dataset用来生成Result,而cache用来生成dataset。至于如何使用dataset生成Result在hashimoto()中讲述,本节介绍如何生成dataset。
dataset和cache中存放的都是伪随机数,每个epoch的区块使用相同的cache和dataset,并且dataset需要暂用大量的内存。刚开始时cache是16MB,dataset是1GB,但每个epoch它们就会增大一次,它们的大小分别定义在datasetSizes和cacheSizes,dataset每次增长8MB,最大能达到16GB,所以挖矿的节点必须有足够大的内存。
使用cache生成dataset。使用cache的部分数据,进行哈希和异或运算,就能生成一组dataset的item,比如下图中的cache中黄色块,能生成dataset中的黄色块,最后把这些Item拼起来就生成了完整的Dataset,完成该功能的函数是generateDataset。
dataset.generate()是dataset的生成函数,该函数只执行一次,先使用generateCache()生成cache,再将cache作为generateDataset()的入参生成dataset,其中需要重点关注的是generateDatasetItem(),该函数是根据部分cache,生成一组dataset item,验证PoW的nonce的时候,也需要使用该函数。
Rand()的实现hashimotoFull()和hashimoto()
hashimotoFull功能是使用dataset、hash和nonce生成Digest和Result。它创建一个获取dataset部分数据的lookup函数,该函数能够返回连续的64字节dataset中的数据,然后把lookup函数、hash和nonce传递给hashimoto。
hashimoto的功能是根据hash和nonce,以及lookup函数生成Digest和Result,lookup函数能够返回64字节的数据就行。它把hash和nonce合成种子,然后根据种子生成混合的数据mix,然后进入一个循环,使用mix和seed获得dataset的行号,使用lookup获取指定行的数据,然后把数据混合到mix中,混合的方式是使用哈希和异或运算,循环结束后再使用哈希和异或函数把mix压缩为64字节,把mix转为小端模式就得到了Digest,把seed和mix进行hash运算得到Result。
1.2.3 如何验证Ethash的随机数
PoW的验证是证明出块人确实进行了大量的哈希计算。Ethash验证区块头中的Nonce和MixDigest是否合法,如果验证通过,则认为出块人确实进行了大量的哈希运算。验证方式是确定区块头中的Nonce是否符合公式,并且区块头中的MixDigest是否与使用此Nonce计算出的是否相同。
验证与挖矿相比,简直是毫不费力,因为:
- 时间节省。验证只进行1次hashimoto运算,而挖矿进行大约Difficulty次。
- 空间节省。验证只需要cache,不需要dataset,也就不需要计算庞大的dataset,因此不挖矿的验证节点,不需要很高的配置。
接下来介绍验证函数VerifySeal(),以及根据cache生成Digest和Result的hashimotoLight()。
验证函数VerifySeal
Ethash.VerifySeal实现PoW验证功能。首先先判断区块中的Difficulty是否匹配,然后生成(获取)当前区块高度的cache,把cache和nonce传递给hashimotoLight,该函数能根据cache, hash, nonce生成Digest和Result,然后校验Digest是否匹配以及Result是否符合条件。
hashimotoLight函数
hashimotoLight使用cache, hash, nonce生成Digest和Result。生成Digest和Result只需要部分的dataset数据,而这些部分dataset数据时可以通过cache生成,因此也就不需要完整的dataset。它把generateDatasetItem函数封装成了获取部分dataset数据的lookup函数,然后传递给hashimoto计算出Digest和Result。
FAQ
Q:每30000个块使用同一个dataset,那可以提前挖出一些合法的Nonce?
A:不行。提前挖去Nonce,意味着还不知道区块头的hash,因此无法生成合法的Nonce。
Q:能否根据符合条件的哈希值,反推出Nonce呢?
A:不行。因为哈希运算具有不可逆性,不能根据摘要反推出明文,同理根据哈希值也无法推出Nonce。