Ethereum当前和Bitcoin同样,采用基于工做量证实(Proof of Work,PoW)的共识算法来产生新的区块。与Bitcoin不一样的是,Ethereum采用的共识算法能够抵御ASIC矿机对挖矿工做的垄断地位,这个算法叫作Ethash
。git
PoW的的核心是Hash运算,谁的Hash运算更快,谁就更有可能挖掘出新的区块,得到更多的经济利益。在Bitcoin的发展过程当中,挖矿设备经历了(CPU=>GPU=>ASIC)的进化过程,其中的动机就是为了更快地进行Hash运算。随着矿机门槛地提升,参与者久愈来愈少,这与区块链的去中心化构想背道而驰。
所以,在共识算法设计时,为了减小ASIC矿机的优点(专用并行计算),Ethereum增长了对于内存的要求,即在进行挖矿的过程当中,须要占用消耗大量的内存空间,而这是ASIC矿机不具有的(配置符合运算那能力的内存太贵了,即便配置,这也就等同于大量CPU了)。即将挖矿算法从CPU密集型(CPU bound)转化为IO密集型(I/O bound)github
Ethash
是从Dagger-Hashimoto
算法改动而来的,而Dagger-Hashimoto
的原型是Thaddeus Dryja提出的Hashimoto算法,它在传统Bitcoin的工做量证实的基础上增长了消耗内存的步骤。算法
传统的PoW的本质是不断尝试不一样的nonce
,计算HASH
$$hash\_output=HASH(prev\_hash,merkle_root,nonce)$$
若是计算结果知足$hash\_output<target$,则说明计算的nonce
是有效的segmentfault
而对于Hashimoto,HASH运算仅仅是第一步,其算法以下:数组
nonce: 64-bits.正在尝试的nonce值 get_txid(T):历史区块上的交易T的hash total_transactions: 历史上的全部交易的个数
hash_output_A = HASH(prev_hash,merkle_root,nonce) for i = 0 to 63 do shifted_A = hash_output_A >> i transaction = shifted_A mod total_transactions txid[i] = get_txit(transaction) << i end of txid_mix = txid[0]^txid[1]...txid[63] final_output = txid_mix ^ (nonce<<192)
能够看出,在进行了HASH运算后,还须要进行64轮的混淆(mix)运算,而混淆的源数据是区块链上的历史交易,矿工节点在运行此算法时,须要访问内存中的历史交易信息(这是内存消耗的来源),最终只有当 $final\_output < target$ 时,才算是找到了有效的nonce
app
Dagger-Hashimoto
相比于Hashimoto,不一样点在于混淆运算的数据源不是区块链上的历史交易,而是以特定算法生成的约1GB大小的数据集合(dataset
),矿工节点在挖矿时,须要将这1GB数据所有载入内存。函数
nonce
填入区块头,还须要填入一项MixDigest
,这是在挖矿过程当中计算出来的,它能够做为矿工的确在进行消耗内存挖矿工做量的证实。验证者在验证区块时也会用到这一项。cache
,约1GB的dataset
由这约16MB的cache
按特定算法生成,dataset中每一项数据都由cache
中的256项数据参与生成,cache
中的这256项数据能够看作是dataset
中数据的parent
。只因此是约,是由于其真正的大小是比16MB和1GB稍微小一点(为了好描述,如下将省略约)cache
和dataset
的内容并不是不变,它每隔一个epoch
(30000个区块)就须要从新计算cache
和dataset
的大小并不是一成不变,16MB和1GB只是初始值,这个大小在每一年会增大73%,这是为了抵消掉摩尔定律下硬件性能的提高,即便硬件性能提高了,那么最终计算所表明的工做量不会变化不少。结合上一条,那么其实每通过30000个区块,cache
和dataset
就会增大一点,而且从新计算cache
和dataset
,而轻客户端只须要存储 cache
。挖矿(seal)时须要dataset
在内存中便于随时存取,而验证(verify)时,只须要有cache就行,须要的dataset
临时计算就行。dataset
经过generate()
方法生成,首先是生成cache,再从cache生成datasetoop
在挖矿与共识中提到了,共识算法经过实现Engine.Seal
接口,来实现挖矿,Ethash算法也不例外。
其顶层流程以下:性能
ethash.mine()
进行实际的挖矿,参数中的block是待挖掘的区块(已经打包好了交易),而nonce
是一个随机值,做为挖矿过程尝试nonce
的初始值。mine()
调用首先计算后续挖矿须要的一些变量。hash为区块头中除了nonce
和mixdigest
的Hash值,dataset为挖掘这个区块时须要的混淆数据集合(占用1GB内存),target是本区块最终Hash须要达到的目标,它与区块难度成反比nonce
进行hashmotoFull()
函数计算最终result
(最终Hash值)和digest
,若是知足target要求,则结束挖矿,不然增长nonce
,再调用hashmotoFull()
func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte) { lookup := func(index uint32) []uint32 { offset := index * hashWords return dataset[offset : offset+hashWords] } return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup) }
hashmotoFull()
是运算的核心,内部调用hashmoto()
,第三个参数为dataset
的大小(即1GB),第四个参数是一个lookup
函数,它接收index参数,返回dataset
中64字节的数据。区块链
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) { // 将dataset划分为2维矩阵,每行mixBytes=128字节,共1073739904/128=8388593行 rows := uint32(size / mixBytes) // 将hash与待尝试的nonce组合成64字节的seed seed := make([]byte, 40) copy(seed, hash) binary.LittleEndian.PutUint64(seed[32:], nonce) seed = crypto.Keccak512(seed) seedHead := binary.LittleEndian.Uint32(seed) // 将64字节的seed转化为32个uint32的mix数组(先后16个uint32内容相同) mix := make([]uint32, mixBytes/4) for i := 0; i < len(mix); i++ { mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:]) } temp := make([]uint32, len(mix)) // 进行总共loopAccesses=64轮的混淆计算,每次计算会去dataset里查询数据 for i := 0; i < loopAccesses; i++ { parent := fnv(uint32(i)^seedHead, mix[i%len(mix)]) % rows for j := uint32(0); j < mixBytes/hashBytes; j++ { copy(temp[j*hashWords:], lookup(2*parent+j)) } fnvHash(mix, temp) } // 压缩mix:将32个uint32的mix压缩成8个uint32 for i := 0; i < len(mix); i += 4 { mix[i/4] = fnv(fnv(fnv(mix[i], mix[i+1]), mix[i+2]), mix[i+3]) } mix = mix[:len(mix)/4] // 用8个uint32的mix填充32字节的digest digest := make([]byte, common.HashLength) for i, val := range mix { binary.LittleEndian.PutUint32(digest[i*4:], val) } // 对seed+digest计算hash,获得最终的hash值 return digest, crypto.Keccak256(append(seed, digest...)) }
验证时VerifySeal()
调用hashimotoLight()
,Light代表验证者不须要完整的dataset,它须要用到的dataset中的数据都是临时从cache中计算。
func hashimotoLight(size uint64, cache []uint32, hash []byte, nonce uint64) ([]byte, []byte) { keccak512 := makeHasher(sha3.NewKeccak512()) //lookup函数和hashimotoFull中的不一样,它调用generateDatasetItem从cache中临时计算 lookup := func(index uint32) []uint32 { rawData := generateDatasetItem(cache, index, keccak512) // return 64 byte data := make([]uint32, len(rawData)/4) // 16 个 uint32 for i := 0; i < len(data); i++ { data[i] = binary.LittleEndian.Uint32(rawData[i*4:]) } return data } return hashimoto(hash, nonce, size, lookup) }
除了lookup
函数不一样,其他部分hashimotoFull
彻底同样
Ethash相比与Bitcoin的挖矿算法,增长了对内存使用的要求,要求矿工提供在挖矿过程当中使用了大量内存的工做量证实,最终达到抵抗ASIC矿机的目的。
1 Ethash-Design-Rationale
2 what-actually-is-a-dag
3 why-dagger-hashimoto-for-ethereum