Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊源码分析—Ethash共识算法

青丝暮雪780
72 0 0
Ethereum当前和Bitcoin一样,采用基于工作量证明(Proof of Work,PoW)的共识算法来产生新的区块。与Bitcoin不同的是,Ethereum采用的共识算法可以抵御ASIC矿机对挖矿工作的垄断地位,这个算法叫做Ethash。
& s4 s, a. \1 M* ?为什么要反ASIC! ]0 q$ S/ A, \0 L+ s
PoW的的核心是Hash运算,谁的Hash运算更快,谁就更有可能挖掘出新的区块,获得更多的经济利益。在Bitcoin的发展过程中,挖矿设备经历了(CPU=>GPU=>ASIC)的进化过程,其中的动机就是为了更快地进行Hash运算。随着矿机门槛地提高,参与者久越来越少,这与区块链的去中心化构想背道而驰。" O$ W5 I& i7 G
因此,在共识算法设计时,为了减少ASIC矿机的优势(专用并行计算),Ethereum增加了对于内存的要求,即在进行挖矿的过程中,需要占用消耗大量的内存空间,而这是ASIC矿机不具备的(配置符合运算那能力的内存太贵了,即使配置,这也就等同于大量CPU了)。即将挖矿算法从CPU密集型(CPU bound)转化为IO密集型(I/O bound)
) O" y, @3 N) |. g- Z; D$ ADagger-Hashimoto
6 O) K8 C7 f  {1 G0 l5 GEthash是从Dagger-Hashimoto算法改动而来的,而Dagger-Hashimoto的原型是Thaddeus Dryja提出的Hashimoto算法,它在传统Bitcoin的工作量证明的基础上增加了消耗内存的步骤。8 q6 y3 @, l$ O( U1 I; H4 n: M7 R9 J
传统的PoW的本质是不断尝试不同的nonce,计算HASH
: ]  {5 R) U/ |$ Ghash_output=HASH(prev_hash,merkleroot,nonce)
% i. g$ x6 i& v6 o5 T6 c2 l, t6 y( Z如果计算结果满足$hash_output4 E/ H. [$ t" _
而对于Hashimoto,HASH运算仅仅是第一步,其算法如下:
9 s: ]% E) u1 bnonce: 64-bits.正在尝试的nonce值+ U9 {; g& K: p+ Q5 Y2 ~
get_txid(T):历史区块上的交易T的hash
# F6 x$ ^0 B* l+ Wtotal_transactions: 历史上的所有交易的个数
/ f) B* X; c9 l; Ahash_output_A = HASH(prev_hash,merkle_root,nonce)
: U% ?; G: \: m! Hfor i = 0 to 63 do
5 B1 I7 t+ \9 S0 }2 k' k" o6 ?    shifted_A = hash_output_A >> i4 R# Z. q  s  D( i5 W) U4 |" Y
    transaction = shifted_A mod total_transactions
# B. |, q/ N1 ?  h5 X0 b* o8 t    txid = get_txit(transaction)
2 x' _* ~4 ]# ^9 R可以看出,在进行了HASH运算后,还需要进行64轮的混淆(mix)运算,而混淆的源数据是区块链上的历史交易,矿工节点在运行此算法时,需要访问内存中的历史交易信息(这是内存消耗的来源),最终只有当 $final_output
/ z' C% j: V  d( y, G7 z# gDagger-Hashimoto相比于Hashimoto,不同点在于混淆运算的数据源不是区块链上的历史交易,而是以特定算法生成的约1GB大小的数据集合(dataset),矿工节点在挖矿时,需要将这1GB数据全部载入内存。( W9 ?  p8 q! h: v% x7 x$ {  Z: p
Ethash算法概要; X5 s# B- e, y/ s; m6 f
矿工挖矿不再是仅仅将找到的nonce填入区块头,还需要填入一项MixDigest,这是在挖矿过程中计算出来的,它可以作为矿工的确在进行消耗内存挖矿工作量的证明。验证者在验证区块时也会用到这一项。先计算出约16MB大小的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临时计算就行。4 ~! I* {' O, _
2 i* d4 P* n9 A, B2 ]7 }% |# L2 I' `
Ethash源码解析; n* a0 y( c& N: ?" p
dataset生成) ^: b/ d- T* I# x# ?. n
dataset通过generate()方法生成,首先是生成cache,再从cache生成dataset
( e9 n/ U2 r1 _# B1 y/ m  K挖矿(Seal)/ R+ e- Z6 r+ h* K1 m( ^$ ]5 D% g3 [
在挖矿与共识中提到了,共识算法通过实现Engine.Seal接口,来实现挖矿,Ethash算法也不例外。
4 V" Y: s2 W, N- |其顶层流程如下:+ a# K6 |& M$ w8 D) P( _$ ]- U) y

) m# s! u4 C. o. ~- y* @" GSeal调用中,启动一个go routine来调用ethash.mine()进行实际的挖矿,参数中的block是待挖掘的区块(已经打包好了交易),而nonce是一个随机值,作为挖矿过程尝试nonce的初始值。mine()调用首先计算后续挖矿需要的一些变量。hash为区块头中除了nonce和mixdigest的Hash值,dataset为挖掘这个区块时需要的混淆数据集合(占用1GB内存),target是本区块最终Hash需要达到的目标,它与区块难度成反比对本次尝试的nonce进行hashmotoFull()函数计算最终result(最终Hash值)和digest,如果满足target要求,则结束挖矿,否则增加nonce,再调用hashmotoFull()# j# B$ i# b+ R
& A, X. }% |( e/ G! g
func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte) {0 V; d' }/ }4 J9 Y+ [
    lookup := func(index uint32) []uint32 {+ I3 T! c/ U+ z! t3 Y
        offset := index * hashWords
! P) Y7 S/ d9 S( Z8 B        return dataset[offset : offset+hashWords]
& q9 ^1 H" i. }5 k5 y. C    }
' R1 y7 i) S6 }/ j5 {0 J    return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup)2 z; d6 ]. c. o7 j: T
}
6 T) ~, S# [2 f2 E- C* IhashmotoFull()是运算的核心,内部调用hashmoto(),第三个参数为dataset的大小(即1GB),第四个参数是一个lookup函数,它接收index参数,返回dataset中64字节的数据。9 S2 E5 e% y3 V6 g6 \) U) M
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {: T6 a* r- V  Z0 P1 G. j
    // 将dataset划分为2维矩阵,每行mixBytes=128字节,共1073739904/128=8388593行9 o! ]" o9 x2 |5 V) H
    rows := uint32(size / mixBytes)! s/ v+ z( i4 u3 E3 k4 C7 q
    # [3 t/ Q4 B( R/ q8 [1 w7 }" L: B6 P
    // 将hash与待尝试的nonce组合成64字节的seed/ c% [; D2 a% ]% @$ q6 N% O9 D- K! r
    seed := make([]byte, 40). G# j* r3 R# {& X% C2 p7 o7 ~; P
    copy(seed, hash)* P" Y8 u. L: V" e$ N
    binary.LittleEndian.PutUint64(seed[32:], nonce)
3 ^3 ~* E+ ~3 k# g    seed = crypto.Keccak512(seed)
3 X( R7 M+ D, Q! j- @) e# d  L    seedHead := binary.LittleEndian.Uint32(seed)2 d' V- ~) _' Y6 U) `1 ]
    // 将64字节的seed转化为32个uint32的mix数组(前后16个uint32内容相同)
0 q, k# y+ I) K0 h0 C6 ~    mix := make([]uint32, mixBytes/4)
% h8 m5 G# A& ?& m0 N    for i := 0; i
1 J4 W" D4 S7 S1 |, e- o7 ]0 i验证(Verify)1 i* `8 e. W& p$ ]4 a6 u
验证时VerifySeal()调用hashimotoLight(),Light表明验证者不需要完整的dataset,它需要用到的dataset中的数据都是临时从cache中计算。1 u& D1 M) @" u9 V1 u
func hashimotoLight(size uint64, cache []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
$ a: O# u& z1 H3 H, Z: u    keccak512 := makeHasher(sha3.NewKeccak512())
9 ]; D7 a5 _, M0 l5 V2 w    //lookup函数和hashimotoFull中的不同,它调用generateDatasetItem从cache中临时计算
8 e" c2 l7 h" P5 J' Y! y3 `    lookup := func(index uint32) []uint32 {
4 A6 \) m  `. \6 Z6 F        rawData := generateDatasetItem(cache, index, keccak512) //  return 64 byte) I9 L. I  l0 F9 D' v
        data := make([]uint32, len(rawData)/4) //  16 个 uint32
+ ^9 k: L4 f- K. z9 Y7 u& E" ?' G9 H        for i := 0; i $ E3 v: i- D3 B, S+ q; A$ V
除了lookup函数不同,其余部分hashimotoFull完全一样8 P/ f+ j/ ?: W& j& q, |$ P
总结
: H  P0 R" q2 GEthash相比与Bitcoin的挖矿算法,增加了对内存使用的要求,要求矿工提供在挖矿过程中使用了大量内存的工作量证明,最终达到抵抗ASIC矿机的目的。/ U9 n2 a; S  w. n9 m9 C5 K

0 R. }8 b9 `$ i2 l6 e# }
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

青丝暮雪780 初中生
  • 粉丝

    0

  • 关注

    2

  • 主题

    11