Ethereum: 曾经为以太坊站稳龙头地位立下悍马功劳的Ethash (PoW) 共识机制

Ethash,以太坊早期的工作量证明共识算法,曾为以太坊的崛起立下汗马功劳。从 2015 年 7 月到 2022 年 9 月,Ethash 的精妙设计和演进历程,依然是理解区块链共识机制的重要里程碑。

概述

Ethash 是以太坊网络早期使用的工作量证明(Proof of Work)共识算法,从 2015 年 7 月的 Frontier 版本开始服务以太坊主网,直到 2022 年 9 月的合并(The Merge)转向权益证明。虽然 Ethash 已经退出历史舞台,但其精妙的设计和演进历程仍然是理解区块链共识机制还是很有价值的。从 Go-Ethereum v1.14+ 开始,Ethash 已完全禁用真正的 PoW 挖矿。

工作流程详解

区块验证流程

Ethash 共识机制的工作流程可以分为三个核心阶段:

  1. 区块验证 - 使用 Ethash Engine 执行严格的区块验证,包括头部属性检查(extra-data 大小、时间戳有效性、区块号连续性)、通过 Difficulty Calculator 进行难度匹配验证,以及 Gas 限制和使用量的合规性检查
  2. 区块最终化 - 验证通过后进入奖励分配流程,系统根据不同硬分叉版本(Frontier 5 ETH → Byzantium 3 ETH → Constantinople 2 ETH)选择相应的区块奖励,循环处理所有叔块以计算叔块奖励和矿工额外收益,最终通过 StateDB 更新相关账户余额
  3. 难度计算 - 系统根据区块高度和硬分叉配置选择合适的难度算法,从 Frontier 的简单 13 秒阈值二元调整,到 Homestead 的 10 秒基准连续调整机制,再到 Byzantium 引入叔块影响因子的动态调整算法,确保网络出块时间的稳定性和安全性

Ethash_Consensus_Workflow-Ethash (PoW) 共识工作流程.png

正式区块的产生与确认

区块产生流程

Ethash 区块的产生主要有四个阶段:

  • 区块准备 矿工通过 Prepare 方法初始化区块头,系统获取父区块信息并计算当前区块的期望难度值,确保新区块符合网络的难度调整规则
  • 区块构建 矿工收集待处理的交易,验证交易的有效性,计算状态根、交易根和收据根,构建完整的区块结构,同时可能包含符合条件的叔块以获得额外奖励。
  • 区块密封 矿工进行工作量证明计算的核心环节,需要找到满足难度要求的 nonce 值,使得区块头的哈希值小于等于目标难度值,这个过程被称为工作量证明(Proof of Work,简称 PoW),是 Ethash 共识机制的核心所在。

    • mine 核心代码
      
      // 历史版本的挖矿核心代码(现已移除)
      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)  // 获取 DAG 数据集
      )

    // 开始 nonce 搜索循环 var ( attempts = int64(0) nonce = seed // 起始 nonce 值 )

search: for { select { case <-abort: // 挖矿被中止 break search

    default:
        attempts++

        // 核心 PoW 计算:使用 Ethash 算法计算哈希
        digest, result := hashimoto(hash, nonce, dataset.size, dataset.lookup)

        // 检查是否满足难度要求
        if new(big.Int).SetBytes(result).Cmp(target) &lt;= 0 {
            // 找到有效 nonce!
            header = types.CopyHeader(header)
            header.Nonce = types.EncodeNonce(nonce)      // 设置 nonce
            header.MixDigest = common.BytesToHash(digest) // 设置混合摘要

            // 返回密封后的区块
            select {
            case found &lt;- block.WithSeal(header):
                // 成功找到并报告
            case &lt;-abort:
                // 找到但被丢弃
            }
            break search
        }
        nonce++  // 尝试下一个 nonce 值
    }
}

}

  - hashimoto 为特定的区块头哈希和 nonce 产生最终值,这是 Ethash 算法的核心函数,实现了内存困难的工作量证明计算

```go
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {
    // 计算理论行数(DAG 数据集的行数)
    // size 是 DAG 数据集的总大小,mixBytes 是每次混合操作的字节数
    rows := uint32(size / mixBytes)

    // 将区块头哈希 + nonce 组合成 40 字节的种子
    // 前 32 字节是区块头哈希,后 8 字节是 nonce(小端序)
    seed := make([]byte, 40)
    copy(seed, hash)                                    // 复制 32 字节的区块头哈希
    binary.LittleEndian.PutUint64(seed[32:], nonce)   // 添加 8 字节的 nonce

    // 使用 Keccak512 生成初始混合种子
    // 这一步将 40 字节种子扩展为 64 字节的初始状态
    seed = crypto.Keccak512(seed)

    // 创建混合数组,用于存储中间计算结果
    // mixBytes/4 是因为每个 uint32 占 4 字节
    mix := make([]uint32, mixBytes/4)

    // 从 64 字节种子初始化混合数组
    // 通过循环复制的方式填充整个 mix 数组
    for i := 0; i &lt; len(mix); i += 4 {
        // 每次处理 4 个 uint32,使用种子的不同部分进行初始化
        binary.LittleEndian.PutUint32(mix[i:], binary.LittleEndian.Uint32(seed[i%len(seed):]))
        binary.LittleEndian.PutUint32(mix[i+1:], binary.LittleEndian.Uint32(seed[(i+1)%len(seed):]))
        binary.LittleEndian.PutUint32(mix[i+2:], binary.LittleEndian.Uint32(seed[(i+2)%len(seed):]))
        binary.LittleEndian.PutUint32(mix[i+3:], binary.LittleEndian.Uint32(seed[(i+3)%len(seed):]))
    }

    // 通过多轮处理混合数组,这是 Ethash 算法的核心循环
    // loops 定义了混合的轮数,增加计算复杂度
    temp := make([]uint32, mixBytes/4)  // 临时数组,存储从 DAG 读取的数据

    for i := 0; i &lt; loops; i++ {  // 外层循环:处理轮数
        // 内层循环:处理每个混合块
        for j := 0; j &lt; mixBytes/hashBytes; j++ {
            // 使用 FNV 哈希函数计算 DAG 数据集的索引
            // 这确保了对 DAG 的访问是伪随机的,增加了内存访问的复杂性
            parent := fnv(uint32(i^j), mix[j]) % rows

            // 从 DAG 数据集中读取数据到临时数组
            // lookup 函数负责从 DAG 中获取指定索引的数据
            copy(temp[j*hashWords:(j+1)*hashWords], lookup(parent))
        }

        // 使用 FNV 哈希将临时数据混合到主混合数组中
        // 这一步将从 DAG 读取的数据与当前状态进行混合
        fnvHash(mix, temp)
    }

    // 压缩混合数组到最终摘要
    // 将大的混合数组压缩为较小的摘要,减少最终输出的大小
    cmix := make([]byte, mixBytes/4)
    for i := 0; i &lt; len(mix); i += 4 {
        // 每 4 个 uint32 压缩为 1 个,使用嵌套的 FNV 哈希
        fnv1 := fnv(fnv(fnv(mix[i], mix[i+1]), mix[i+2]), mix[i+3])
        binary.LittleEndian.PutUint32(cmix[i/4:], fnv1)
    }

    // 返回最终的摘要和结果
    digest := cmix  // 混合摘要,存储在区块头的 MixDigest 字段中

    // 最终结果:将原始种子(前32字节)与压缩摘要连接后进行 Keccak256 哈希
    // 这个结果用于与难度目标进行比较,判断 PoW 是否有效
    result := crypto.Keccak256(append(seed[:32], digest...))

    return digest, result
}
  • 区块最终化 通过验证的区块进入 Finalize 流程,系统根据硬分叉版本分配相应的区块奖励,处理叔块奖励计算,通过 StateDB 更新矿工账户余额,完成区块的最终确认

Ethash_Block_Production_Sequence-Ethash 区块产生与确认.png

叔块机制

  1. 叔块(Uncle Block)概念源于以太坊对比特币孤块问题的改进。在比特币网络中,由于网络延迟和传播时间,多个矿工可能同时挖出有效区块,但只有一个能成为主链的一部分,其他区块被称为"孤块"并被完全丢弃,导致算力浪费和网络安全性降低,以太坊设计了叔块机制来解决这一问题。

  2. 叔块奖励公式:((叔块号 + 8) - 当前区块号) × 基础奖励 ÷ 8

    • 叔块越新(距离当前区块越近),获得的奖励越高
    • 最新的叔块(距离1代)可获得基础奖励的7/8
    • 最旧的叔块(距离7代)可获得基础奖励的1/8
  3. 叔块验证机制:首先执行基础检查,然后进行祖先信息收集,接着标记当前区块防止自引用攻击,最后逐个验证每个叔块的重复性、祖先关系、父区块有效性和头部完整性,只有通过全部检查的叔块才能被接受并获得奖励,这种严格的多层验证机制有效防止了重复包含、祖先攻击等安全威胁,同时确保叔块在合理时间窗口内的公平性。

  4. 叔块验证流程

Ethash_Uncle_Verification_Sequence-Ethash 叔块验证.png

难度调整算法

Ethash 的难度调整算法经历了三个主要版本的演进,每个版本都针对特定问题进行了优化。

版本 生效时间 代码实现 主要特点
Frontier 2015年7月30日 calcDifficultyFrontier 创世版本,简单二元调整
Homestead 2016年3月14日 calcDifficultyHomestead EIP-2 改进,连续调整机制
Byzantium+ 2017年10月16日起 makeDifficultyCalculator 动态版本,叔块影响因子

Frontier 算法 (2015年7月)

数学公式

difficulty = base_difficulty + difficulty_bomb

其中:
base_difficulty = {
    parent_difficulty + adjustment,  if (time - parent.time) &lt; 13
    parent_difficulty - adjustment,  if (time - parent.time) ≥ 13
}
adjustment = parent_difficulty / 2048

difficulty_bomb = {
    0,                           if periodCount ≤ 1
    2^(periodCount - 2),        if periodCount > 1
}
periodCount = (parent.number + 1) / 100000

最终约束:difficulty = max(calculated_difficulty, 131072)

设计特点:最简单的难度调整,仅基于出块时间

  • 简单的二元调整:仅基于13秒阈值
  • 固定调整幅度:parent_difficulty / 2048 ≈ 0.05% 调整
  • 早期难度炸弹:基于当前区块号计算,推动网络升级
  • 最小难度保护:确保网络安全的基础难度

Homestead 算法 (2016年3月)

数学公式

原公式:
 diff = (parent_diff +
             (parent_diff / 2048 * max(1 - (block_timestamp - parent_timestamp) // 10, -99))
            ) + 2^(periodCount - 2)

difficulty = parent_difficulty + base_adjustment + difficulty_bomb

其中:
base_adjustment = parent_diff / 2048 * adjustment_factor
adjustment_factor = max(1 - (block_timestamp - parent_timestamp) // 10, -99)
difficulty_bomb = 2^(periodCount - 2)

'//' 这里表示整数除法

设计特点

  • 连续调整:从二元调整改为基于时间差的连续调整
  • 时间窗口:从13秒阈值改为10秒基准时间
  • 调整范围:限制最大负调整为-99,防止难度急剧下降
  • 更平滑:避免了13秒阈值附近的难度跳跃

Byzantium+ 动态算法 (2017年10月起)

数学公式

diff = (parent_diff +
        (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99))
       ) + 2^(periodCount - 2)

其中:
1. 基础难度调整 = parent_diff / 2048 * 时间调整因子
2. 时间调整因子 = max(叔块因子 - 时间差因子, -99)
3. 叔块因子 = 2 (有叔块) 或 1 (无叔块)
4. 时间差因子 = (当前时间戳 - 父区块时间戳) // 9    (注: // 是整数除法)
5. 难度炸弹 = 2^(periodCount - 2)

设计特点

  • 考虑叔块影响,更精确的时间调整,可配置的难度炸弹延迟
  • 支持通过硬分叉延迟难度炸弹生效时间

算法选择

以太坊的难度计算系统通过 CalcDifficulty 函数实现智能化的算法选择,根据区块高度和硬分叉配置自动选择最适合的计算方法。

硬分叉版本 难度计算方式 激活区块号 生效时间 EIP 编号 核心改进 难度炸弹状态
Frontier FrontierDifficultyCalculator 0 2015年7月 - 基础 PoW 算法,创世版本,简单二元调整 首次引入
Homestead HomesteadDifficultyCalculator 1,150,000 2016年3月 EIP-2 连续调整 + 首个难度炸弹 激活
Byzantium DynamicDifficultyCalculator 4,370,000 2017年10月 EIP-649 叔块影响 + 难度炸弹 延迟 300万块
Constantinople DynamicDifficultyCalculator 7,280,000 2019年2月 EIP-1234 区块奖励减少 + 难度炸弹延迟 延迟 500万块
Muir Glacier DynamicDifficultyCalculator 9,200,000 2020年1月 EIP-2384 仅延迟炸弹 延迟 400万块
London DynamicDifficultyCalculator 12,965,000 2021年8月 EIP-3554 EIP-1559 + 难度炸弹延迟 延迟至2021年12月
Arrow Glacier DynamicDifficultyCalculator 13,773,000 2021年12月 EIP-4345 仅延迟炸弹 延迟至2022年6月
Gray Glacier DynamicDifficultyCalculator 15,050,000 2022年6月 EIP-5133 最后延迟 延迟至2022年9月

奖励机制

奖励结构

Ethash 的奖励机制包含区块奖励和叔块奖励两部分,通过 accumulateRewards 函数实现完整的奖励分配逻辑。

区块奖励演进

硬分叉版本 区块奖励 生效时间 设计目的
Frontier 5 ETH 2015年7月 初始激励机制
Byzantium 3 ETH 2017年10月 降低通胀率
Constantinople 2 ETH 2019年2月 为PoS转换做准备

矿工额外奖励机制

奖励类型 计算公式 备注
基础区块奖励 根据硬分叉版本 5/3/2 ETH
叔块包含奖励 基础奖励 ÷ 32 × 叔块数量 每个叔块奖励 1/32
总奖励发放 基础奖励 + 叔块奖励 给区块矿工

总结

Ethash 作为以太坊早期的共识机制,成功支撑了网络从实验性项目发展为全球第二大区块链平台。其精妙的难度调整算法和叔块激励机制为后续的共识机制设计提供了重要参考。

虽然 Ethash 已经完成历史使命,但其设计思想和技术实现仍然是理解区块链共识机制演进的重要里程碑。从 Frontier 的简单二元调整到 Byzantium 的复杂动态算法,体现了以太坊社区在技术创新和网络治理方面的不断探索和改进。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
一眼万年
一眼万年
微信公众号:chaincat 欢迎关注