ETHASH
Ethash 是在现已弃用的工作量证明架构下,实际用于真正的以太坊主网的挖矿算法。
Ethash 实际上是为 Dagger Hashimoto 算法进行重要更新后的一个特殊版本命名的新名称,但它仍然继承了其前身的基本原理。
以太坊主网只使用过 Ethash - Dagger Hashimoto 是挖矿算法的研发版本,在以太坊主网上开始挖矿之前被取代。
是什么?
Ethash 是 Dagger-Hashimoto 算法的修改版。
Ethash 工作量证明是内存密集型算法,这被认为使算法可抵御专用集成电路。
Ethash 专用集成电路最终被开发出来,但在工作量证明被关闭之前,图形处理单元挖矿仍然是一个可行的选择。
Ethash 仍然用于在其他非以太坊工作量证明网络上挖掘其他币。
ETHASH 是如何工作的?
内存硬度通过工作量证明算法实现,需要根据随机数和区块头选择固定资源子集。
该资源(大小为几 GB)称为有向无环图。
有向无环图每 30000 个区块更改一次(大约 125 小时的窗口,称为一个时段(大约 5.2 天)),需要一段时间才能生成。
由于有向无环图仅依赖于区块高度,因此可以预先生成,但如果没有,则客户端需要等到此过程结束才能生成区块。
如果客户端没有提前预生成和缓存有向无环图,网络可能会在每个时段过渡时遇到严重的区块延迟。
请注意,不需要生成有向无环图即可验证,工作量证明本质上允许使用低端中央处理器和小内存进行验证。
该算法采取的一般路线如下:
-
有一个种子,可以通过扫描区块头直到该点来为每个区块计算种子。
-
从种子中可以计算出 16 MB 的伪随机缓存。 轻量级客户端存储缓存。
-
我们可以从缓存中生成一个 1 GB 数据集,数据集中每个项目仅依赖于一小部分缓存中的项目。 全客户端和矿工存储数据集。 数据集随着时间的流逝而呈线性增长。
-
采矿会抢走数据集的随机片段并将它们散列在一起。 可以通过使用缓存来重新生成您需要的数据集中的特定区块,以较低的内存进行验证,以使您只需要存储缓存。
每隔 30000 个区块更新一次大数据集,因此,矿工的绝大部分工作都是读取数据集,而不是对其进行修改。
定义
我们采用以下定义:
WORD_BYTES = 4 # bytes in word
DATASET_BYTES_INIT = 2**30 # bytes in dataset at genesis
DATASET_BYTES_GROWTH = 2**23 # dataset growth per epoch
CACHE_BYTES_INIT = 2**24 # bytes in cache at genesis
CACHE_BYTES_GROWTH = 2**17 # cache growth per epoch
CACHE_MULTIPLIER=1024 # Size of the DAG relative to the cache
EPOCH_LENGTH = 30000 # blocks per epoch
MIX_BYTES = 128 # width of mix
HASH_BYTES = 64 # hash length in bytes
DATASET_PARENTS = 256 # number of parents of each dataset element
CACHE_ROUNDS = 3 # number of rounds in cache production
ACCESSES = 64 # number of accesses in hashimoto loop
参数
Ethash 的缓存和数据集的参数取决于区块号。
缓存大小和数据集大小都呈线性增长;然而,我们总是取低于线性增长阈值的最高素数,以降低意外规律导致循环行为的风险。
def get_cache_size(block_number):
sz = CACHE_BYTES_INIT + CACHE_BYTES_GROWTH * (block_number // EPOCH_LENGTH)
sz -= HASH_BYTES
while not isprime(sz / HASH_BYTES):
sz -= 2 * HASH_BYTES
return sz
def get_full_size(block_number):
sz = DATASET_BYTES_INIT + DATASET_BYTES_GROWTH * (block_number // EPOCH_LENGTH)
sz -= MIX_BYTES
while not isprime(sz / MIX_BYTES):
sz -= 2 * MIX_BYTES
return sz
附录中提供了数据集和缓存大小值表。
缓存生成
现在,我们来指定生成缓存的函数:
def mkcache(cache_size, seed):
n = cache_size // HASH_BYTES
# Sequentially produce the initial dataset
o = [sha3_512(seed)]
for i in range(1, n):
o.append(sha3_512(o[-1]))
# Use a low-round version of randmemohash
for _ in range(CACHE_ROUNDS):
for i in range(n):
v = o[i][0] % n
o[i] = sha3_512(map(xor, o[(i-1+n) % n], o[v]))
return o
缓存生成过程中,先按顺序填充 32 MB 内存,然后从严格内存硬哈希函数 (2014) 执行两次 Sergio Demian Lerner 的 RandMemoHash 算法。
输出一组 524288 个 64 字节值。
数据聚合函数
我们使用灵感来自 FNV 哈希的算法,在部分情况下,这种算法可用作逻辑异或的不相关替代。
请注意,我们使用全 32 位输入乘以素数,与之相对地,FNV-1 spec 用 1 个字节(8 个字节)依次乘以素数。
FNV_PRIME = 0x01000193
def fnv(v1, v2):
return ((v1 * FNV_PRIME) ^ v2) % 2**32
请注意,即使黄皮书也指出 fnv 为 v1*(FNV_PRIME ^ v2)
,所有当前实现始终采用上述定义。
完整数据集计算
整个 1 GB 数据集中每个 64 字节项目的计算如下:
def calc_dataset_item(cache, i):
n = len(cache)
r = HASH_BYTES // WORD_BYTES
# initialize the mix
mix = copy.copy(cache[i % n])
mix[0] ^= i
mix = sha3_512(mix)
# fnv it with a lot of random cache nodes based on i
for j in range(DATASET_PARENTS):
cache_index = fnv(i ^ j, mix[j % r])
mix = map(fnv, mix, cache[cache_index % n])
return sha3_512(mix)
基本上,我们将来自 256 个伪随机选择的缓存节点的数据聚集起来求哈希值,以计算数据集节点。
然后生成整个数据集:
def calc_dataset(full_size, cache):
return [calc_dataset_item(cache, i) for i in range(full_size // HASH_BYTES)]
主循环
现在,我们指定了类似“hashimoto”的主要循环。
在此循环中,我们聚合整个数据集的数据,以生成特定区块头和随机数的最终值。
在下面的代码中,header 代表一个被截断区块头的递归长度前缀表示的 SHA3-256 哈希值。
被截断是指区块头被截去了 mixHash 和随机数字段。
nonce 是指一个 64 位无符号整数的八个字节,按大端序排列。
因此 nonce[::-1]
是上述值的八字节小端序表示:
def hashimoto(header, nonce, full_size, dataset_lookup):
n = full_size / HASH_BYTES
w = MIX_BYTES // WORD_BYTES
mixhashes = MIX_BYTES / HASH_BYTES
# combine header+nonce into a 64 byte seed
s = sha3_512(header + nonce[::-1])
# start the mix with replicated s
mix = []
for _ in range(MIX_BYTES / HASH_BYTES):
mix.extend(s)
# mix in random dataset nodes
for i in range(ACCESSES):
p = fnv(i ^ s[0], mix[i % w]) % (n // mixhashes) * mixhashes
newdata = []
for j in range(MIX_BYTES / HASH_BYTES):
newdata.extend(dataset_lookup(p + j))
mix = map(fnv, mix, newdata)
# compress mix
cmix = []
for i in range(0, len(mix), 4):
cmix.append(fnv(fnv(fnv(mix[i], mix[i+1]), mix[i+2]), mix[i+3]))
return {
"mix digest": serialize_hash(cmix),
"result": serialize_hash(sha3_256(s+cmix))
}
def hashimoto_light(full_size, cache, header, nonce):
return hashimoto(header, nonce, full_size, lambda x: calc_dataset_item(cache, x))
def hashimoto_full(full_size, dataset, header, nonce):
return hashimoto(header, nonce, full_size, lambda x: dataset[x])
基本上,我们保持着一个宽 128 字节的“混合物”,并多次按顺序从整个数据集中获取 128 字节,并使用 fnv 函数将其与混合物结合起来。
使用 128 字节的序列访问,以便每轮算法总是能从随机访问内存获取完整的页面,从而尽量减少转译后备缓冲区的疏忽,而专用集成电路在理论上能够避免这些疏忽。
如果此算法的输出低于所需目标,即证明随机数是有效的。
请注意,在最后额外应用 sha3_256 将确保中间随机数的存在。
提供此证据可以证明至少做了少量工作;而且此快速外部工作量证明验证可以用于反分布式拒绝服务目的。
也可提供统计保证,说明结果是一个无偏 256 位数字。
挖矿
挖矿算法定义如下:
def mine(full_size, dataset, header, difficulty):
# zero-pad target to compare with hash on the same digit
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
请注意,为了顺利挖矿和验证,我们建议在单个线程中预先计算未来的种子哈希值和数据集。
参考资料
https://ethereum.org/zh/developers/docs/consensus-mechanisms/pow/mining/
https://ethereum.org/zh/developers/docs/consensus-mechanisms/pow/mining-algorithms/
https://ethereum.org/zh/developers/docs/consensus-mechanisms/pow/mining-algorithms/ethash/
https://ethereum.org/zh/developers/docs/consensus-mechanisms/pow/mining-algorithms/ethash