主页 > imtoken国际版 > 以太坊源码分析之共识算法ethash

以太坊源码分析之共识算法ethash

imtoken国际版 2023-09-02 05:09:46

区块链是作为分布式系统构建的,由于不依赖于中央权威,分散的节点需要就交易是否有效达成一致,而达成一致的机制就是共识算法。

以太坊目前的算法是一种类 POW 算法:ethash。 除了像比特币一样需要消耗计算机资源进行计算外,还考虑了对专用矿机的抵抗力,增加了挖矿的公平性。

通用 POW 算法思路

POW即Proof of Work,就是通过工作的结果证明你已经完成了相应的工作。

其初衷是人人可参与以太坊公认共识端,门槛低(容易验证),但出结果难(计算复杂)。 在这一点上,只匹配部分特征的哈希算法(不可逆)就非常适合。

哈希值是通过不断改变随机数得到的,比较是否小于给定的值(难度),匹配的就是合适的结果。

随机数一般来自区块头结构中的nonce域。

因为出块时间是一定的,但是整体算力是不确定的,所以难度一般是根据时间来调整的。

ethash算法的思想

Ethash 类似于 pow。 数据来源不仅像比特币一样来自header structure和nonce,还有自己的一套数据集。

简化后的核心代码如下:

func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {
    var (
        header  = block.Header()
        hash    = ethash.SealHash(header).Bytes()
        target  = new(big.Int).Div(two256, header.Difficulty)
        number  = header.Number.Uint64()
        dataset = ethash.dataset(number, false)
    )
    var (
        attempts = int64(0)
        nonce    = seed
    )
    for {
            attempts++
            digest, result := hashimotoFull(dataset.dataset, hash, nonce)
            if new(big.Int).SetBytes(result).Cmp(target) <= 0 {
                // Correct nonce found, create a new header with it
                header = types.CopyHeader(header)
                header.Nonce = types.EncodeNonce(nonce)
                header.MixDigest = common.BytesToHash(digest)
                ...
            }
            ...
      }
      ...
}

    for i := 0; i < threads; i++ {
        pend.Add(1)
        go func(id int, nonce uint64) {
            defer pend.Done()
            ethash.mine(block, id, nonce, abort, locals)
        }(i, uint64(ethash.rand.Int63()))
    }

miner方法中,hashimotoFull返回result,result seed-->cache-->dataset,生成一致的结果。

sitecsdn.net 以太坊和以太币的关系_sitejianshu.com 以太坊以太经典_以太坊公认共识端

图片.png

从这个图中可以看出,无论是挖矿节点还是验证节点(全规模验证),生成数据集调用的方法是一样的,参数个数块高是一样的,所以数据集也是一样(参数async在这里不影响以太坊公认共识端,如果为true,数据集会在后台生成)。

桥本

Hashimoto 聚合数据集、块头哈希和随机数以生成最终值。

sitecsdn.net 以太坊和以太币的关系_以太坊公认共识端_sitejianshu.com 以太坊以太经典

图片来自简书App

如图所示:

桥本全功能

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)
}

hashtoLight 函数

func hashimotoLight(size uint64, cache []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
    keccak512 := makeHasher(sha3.NewLegacyKeccak512())
    lookup := func(index uint32) []uint32 {
        rawData := generateDatasetItem(cache, index, keccak512)
        data := make([]uint32, len(rawData)/4)
        for i := 0; i < len(data); i++ {
            data[i] = binary.LittleEndian.Uint32(rawData[i*4:])
        }
        return data
    }
    return hashimoto(hash, nonce, size, lookup)
}

桥本函数:多次聚合header hash、nonce、dataset,得到最终的取值结果

func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {
    // Calculate the number of theoretical rows (we use one buffer nonetheless)
    rows := uint32(size / mixBytes)
    // Combine header+nonce into a 64 byte seed
    seed := make([]byte, 40)
    copy(seed, hash)
    binary.LittleEndian.PutUint64(seed[32:], nonce)
    seed = crypto.Keccak512(seed)
    seedHead := binary.LittleEndian.Uint32(seed)
    // Start the mix with replicated seed
    mix := make([]uint32, mixBytes/4)
    for i := 0; i < len(mix); i++ {
        mix[i] = binary.LittleEndian.Uint32(seed[i*4:])
    }
    // Mix in random dataset nodes
    temp := make([]uint32, len(mix))
    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)
    }
    // Compress mix
    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]
    digest := make([]byte, common.HashLength)
    for i, val := range mix {
        binary.LittleEndian.PutUint32(digest[i*4:], val)
    }
    return digest, crypto.Keccak256(append(seed, digest...))
}

consensus\ethash\algorithm.go中的generateCache、generateDataset、generateDatasetItem、hashimoto函数,这里只提供一个大概的思路,不去深究,因为涉及到一些密码算法,我自己看不懂代码。 另外,这部分细节对整体理解ethash算法影响不大。 以后还是分析密码算法比较好。

困难

func CalcDifficulty(config *params.ChainConfig, time uint64, parent *types.Header) *big.Int {
    next := new(big.Int).Add(parent.Number, big1)
    switch {
    case config.IsConstantinople(next):
        return calcDifficultyConstantinople(time, parent)
    case config.IsByzantium(next):
        return calcDifficultyByzantium(time, parent)
    case config.IsHomestead(next):
        return calcDifficultyHomestead(time, parent)
    default:
        return calcDifficultyFrontier(time, parent)
    }
}

以太坊的难度分多个阶段调整,根据区块高度的阶段使用该阶段的计算公式。 由于目前处于君士坦丁堡阶段,因此仅描述代码。

计算君士坦丁堡难度的函数是makeDifficultyCalculator。 由于评论里已经有对应的公式,代码就不贴了。 梳理如下:

step = parent_diff // 2048

direction = max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99)

periodCount = (block.number - (5000000-1)) if (block.number >= (5000000-1)) else 0

diff = parent_diff + step *direction+ 2^(periodCount - 2)

diffculty计算分为两部分: 1.动态线性调整:parent_diff + step *direction

这部分难度值是在父块的基础上微调的。 调整单位为step,根据当前块与父块的时间间隔,是否有叔块得到一个单位调整范围方向。 step *direction 为调整值。

也就是说,时间间隔太小,方向为正,难度会增加一点,间隔太小,难度会降低一点。 为了防止一直产生叔块,会延长时间,增加难度。 如果出现意外情况或程序出现漏洞,难度大大降低,并有最低门槛(-99)。

值得注意的是,parent_diff + step *direction 除了方向有最小阈值外,还有一个最小难度。

        // minimum difficulty can ever be (before exponential factor)
        if x.Cmp(params.MinimumDifficulty) < 0 {
            x.Set(params.MinimumDifficulty)
        }

MinimumDifficulty      = big.NewInt(131072) // The minimum that the difficulty may ever be.

2. 指数增长:2^(periodCount - 2)

指数增长部分在以太坊中被称为难度炸弹(也称为冰河时代)。 由于以太坊的共识算法会从pow过渡到pos,设计难度炸弹会使挖矿难度越来越大,以至于无法出块。 这样矿工就有了期待,和平过渡到pos。

目前在君士坦丁堡阶段,periodCount减去数字5,000,000是为了防止指数部分增长过快,即延迟难度炸弹生效。

关于header.difficulty的具体难度分析,这篇文章分析的很好。

试验结果

我本地私链当前区块高度为2278,配置的君士坦丁堡起始高度为5,即进入君士坦丁堡阶段。

刚开始程序挖矿的时候,因为没有其他节点挖矿,最后一个出块时间和当前出块时间的间隔相差很大,方向设置为-99。 但是区块高度不超过5000000,索引部分结果为0,父难度为689054,最终难度为655790。

sitecsdn.net 以太坊和以太币的关系_sitejianshu.com 以太坊以太经典_以太坊公认共识端

图片.png

一段时间后,方向基本徘徊在-1、0、1之间,平均挖矿间隔只有十几秒。

sitecsdn.net 以太坊和以太币的关系_sitejianshu.com 以太坊以太经典_以太坊公认共识端

图片.png

INFO [06-09|17:49:46.059] luopeng2                                 block_timestamp - parent_timestamp=25
INFO [06-09|17:49:53.529] luopeng2                                 block_timestamp - parent_timestamp=7
INFO [06-09|17:51:19.667] luopeng2                                 block_timestamp - parent_timestamp=18
INFO [06-09|17:51:23.387] luopeng2                                 block_timestamp - parent_timestamp=4
INFO [06-09|18:46:02.608] luopeng2                                 block_timestamp - parent_timestamp=31
INFO [06-09|18:46:18.575] luopeng2                                 block_timestamp - parent_timestamp=1

目前以太坊网络状态正常,算力稳定,ethgasstation

网站上查到的时间间隔和我当地的情况差不多

sitecsdn.net 以太坊和以太币的关系_sitejianshu.com 以太坊以太经典_以太坊公认共识端

图片.png

总结

综上所述,ethash的基本思想类似于比特币的pow,就是比较随机nonce得到的值和难度。 如果条件满足则挖矿成功,否则继续尝试。 与比特币竞争CPU算力不同,ethash通过生成庞大的数据集和限制内存来增强去中心化,防止ASIC矿机强势算力的垄断。

参考