跳转到主要内容
Change page

Ethash

Ethash 曾是以太坊的工作量证明 (PoW) 挖矿算法。工作量证明现已完全关闭,以太坊现在使用权益证明 (PoS)来保障安全。了解更多关于合并权益证明质押的信息。本页面仅供历史参考!

Ethash 是 Dagger-Hashimoto 算法的修改版本。Ethash 工作量证明是内存困难 (memory hard) (opens in a new tab)的,这曾被认为能使该算法抗 ASIC。最终还是开发出了 Ethash ASIC,但在工作量证明关闭之前,GPU 挖矿仍然是一个可行的选择。Ethash 仍被用于在其他非以太坊工作量证明网络上挖掘其他代币。

Ethash 是如何工作的?

内存困难是通过一种工作量证明算法实现的,该算法要求根据随机数和区块头选择固定资源的子集。这种资源(大小为几 GB)被称为 DAG。DAG 每 30000 个区块更改一次,这个大约 125 小时的窗口期被称为一个时段(约 5.2 天),并且生成 DAG 需要一段时间。由于 DAG 仅取决于区块高度,因此可以预先生成,但如果没有预先生成,客户端需要等到此过程结束才能生成区块。如果客户端不提前预生成并缓存 DAG,网络在每次时段转换时可能会经历大规模的区块延迟。请注意,验证工作量证明不需要生成 DAG,这实际上允许在低 CPU 和小内存的情况下进行验证。

该算法的一般流程如下:

  1. 存在一个种子 (seed),可以通过扫描到该点为止的区块头为每个区块计算得出。
  2. 根据种子,可以计算出一个 16 MB 的伪随机缓存。轻客户端存储该缓存。
  3. 根据缓存,我们可以生成一个 1 GB 的数据集,其特性是数据集中的每个项目仅依赖于缓存中的少量项目。全节点客户端和矿工存储该数据集。数据集随时间线性增长。
  4. 挖矿涉及抓取数据集的随机切片并将它们一起进行哈希处理。验证可以在低内存下完成,方法是使用缓存重新生成所需的特定数据集片段,因此你只需要存储缓存。

大型数据集每 30000 个区块更新一次,因此矿工的绝大部分工作将是读取数据集,而不是对其进行更改。

定义

我们采用以下定义:

“SHA3”的使用

以太坊的开发与 SHA3 标准的开发同时进行,而标准制定过程在最终确定的哈希算法的填充方面做出了较晚的更改,因此以太坊的“sha3_256”和“sha3_512”哈希不是标准的 sha3 哈希,而是在其他上下文中通常被称为“Keccak-256”和“Keccak-512”的变体。请参阅讨论,例如这里 (opens in a new tab)这里 (opens in a new tab)这里 (opens in a new tab)

请记住这一点,因为在下面算法的描述中提到了“sha3”哈希。

参数

Ethash 缓存和数据集的参数取决于区块号。缓存大小和数据集大小都呈线性增长;然而,我们总是取低于线性增长阈值的最大素数,以降低偶然规律性导致循环行为的风险。

附录中提供了数据集和缓存大小值的表格。

缓存生成

现在,我们指定用于生成缓存的函数:

缓存生成过程包括首先按顺序填满 32 MB 内存,然后执行两次 Sergio Demian Lerner 在 Strict Memory Hard Hashing Functions (2014) (opens in a new tab) 中提出的 RandMemoHash 算法。输出是一组 524288 个 64 字节的值。

数据聚合函数

在某些情况下,我们使用受 FNV 哈希 (opens in a new tab)启发的算法作为异或 (XOR) 的非结合替代方案。请注意,我们将素数与完整的 32 位输入相乘,这与 FNV-1 规范依次将素数与一个字节(八位字节)相乘的做法不同。

FNV_PRIME = 0x01000193

def fnv(v1, v2):
    return ((v1 * FNV_PRIME) ^ v2) % 2**32

请注意,即使黄皮书将 fnv 指定为 v1*(FNV_PRIME ^ v2),所有当前的实现都一致使用上述定义。

完整数据集计算

完整的 1 GB 数据集中的每个 64 字节项的计算方式如下:

本质上,我们结合来自 256 个伪随机选择的缓存节点的数据,并对其进行哈希处理以计算数据集节点。然后通过以下方式生成整个数据集:

def calc_dataset(full_size, cache):
    return [calc_dataset_item(cache, i) for i in range(full_size // HASH_BYTES)]

主循环

现在,我们指定类似“hashimoto”的主循环,我们在其中聚合来自完整数据集的数据,以便为特定的区块头和随机数生成最终值。在下面的代码中,header 表示_截断的_区块头的 RLP 表示形式的 SHA3-256 哈希,即不包括 mixHashnonce 字段的区块头。nonce 是大端序的 64 位无符号整数的八个字节。因此 nonce[::-1] 是该值的八字节小端序表示形式:

本质上,我们维护一个 128 字节宽的“混合 (mix)”,并重复按顺序从完整数据集中获取 128 字节,然后使用 fnv 函数将其与混合结合。使用 128 字节的顺序访问,以便算法的每一轮始终从 RAM 中获取一个完整的页面,从而最大限度地减少转换后备缓冲区 (TLB) 未命中,而 ASIC 在理论上能够避免这种情况。

如果该算法的输出低于所需目标,则随机数有效。请注意,最后额外应用 sha3_256 确保存在一个中间随机数,可以提供该随机数来证明至少完成了一小部分工作;这种快速的外部工作量证明 (PoW) 验证可用于防范 DDoS 攻击。它还用于提供统计保证,确保结果是一个无偏的 256 位数字。

挖矿

挖矿算法定义如下:

def mine(full_size, dataset, header, difficulty):
    # 将目标补零,以便在相同位数上与哈希进行比较
    target = zpad(encode_int(2**256 // difficulty), 64)[::-1]
    from random import randint
    nonce = randint(0, 2**64)
    while hashimoto_full(full_size, dataset, header, nonce) > target:
        nonce = (nonce + 1) % 2**64
    return nonce

定义种子哈希

为了计算用于在给定区块之上进行挖矿的种子哈希,我们使用以下算法:

 def get_seedhash(block):
     s = '\x00' * 32
     for i in range(block.number // EPOCH_LENGTH):
         s = serialize_hash(sha3_256(s))
     return s

请注意,为了顺利进行挖矿和验证,我们建议在单独的线程中预先计算未来的种子哈希和数据集。

延伸阅读

知道对您有帮助的社区资源吗?编辑本页面并添加它!

附录

如果您有兴趣将上述 Python 规范作为代码运行,则应在前面添加以下代码。

数据大小

以下查找表提供了大约 2048 个时段的数据大小和缓存大小的表格数据。