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 共识机制的工作流程可以分为三个核心阶段:
区块产生流程:
Ethash 区块的产生主要有四个阶段:
Prepare
方法初始化区块头,系统获取父区块信息并计算当前区块的期望难度值,确保新区块符合网络的难度调整规则区块密封 矿工进行工作量证明计算的核心环节,需要找到满足难度要求的 nonce 值,使得区块头的哈希值小于等于目标难度值,这个过程被称为工作量证明(Proof of Work,简称 PoW),是 Ethash 共识机制的核心所在。
// 历史版本的挖矿核心代码(现已移除)
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) <= 0 {
// 找到有效 nonce!
header = types.CopyHeader(header)
header.Nonce = types.EncodeNonce(nonce) // 设置 nonce
header.MixDigest = common.BytesToHash(digest) // 设置混合摘要
// 返回密封后的区块
select {
case found <- block.WithSeal(header):
// 成功找到并报告
case <-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 < 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 < loops; i++ { // 外层循环:处理轮数
// 内层循环:处理每个混合块
for j := 0; j < 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 < 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 更新矿工账户余额,完成区块的最终确认叔块(Uncle Block)概念源于以太坊对比特币孤块问题的改进。在比特币网络中,由于网络延迟和传播时间,多个矿工可能同时挖出有效区块,但只有一个能成为主链的一部分,其他区块被称为"孤块"并被完全丢弃,导致算力浪费和网络安全性降低,以太坊设计了叔块机制来解决这一问题。
叔块奖励公式:((叔块号 + 8) - 当前区块号) × 基础奖励 ÷ 8
叔块验证机制:首先执行基础检查,然后进行祖先信息收集,接着标记当前区块防止自引用攻击,最后逐个验证每个叔块的重复性、祖先关系、父区块有效性和头部完整性,只有通过全部检查的叔块才能被接受并获得奖励,这种严格的多层验证机制有效防止了重复包含、祖先攻击等安全威胁,同时确保叔块在合理时间窗口内的公平性。
叔块验证流程
Ethash 的难度调整算法经历了三个主要版本的演进,每个版本都针对特定问题进行了优化。
版本 | 生效时间 | 代码实现 | 主要特点 |
---|---|---|---|
Frontier | 2015年7月30日 | calcDifficultyFrontier |
创世版本,简单二元调整 |
Homestead | 2016年3月14日 | calcDifficultyHomestead |
EIP-2 改进,连续调整机制 |
Byzantium+ | 2017年10月16日起 | makeDifficultyCalculator |
动态版本,叔块影响因子 |
数学公式:
difficulty = base_difficulty + difficulty_bomb
其中:
base_difficulty = {
parent_difficulty + adjustment, if (time - parent.time) < 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)
设计特点:最简单的难度调整,仅基于出块时间
数学公式:
原公式:
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)
'//' 这里表示整数除法
设计特点
数学公式:
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 的复杂动态算法,体现了以太坊社区在技术创新和网络治理方面的不断探索和改进。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!