EIP-1588 硬分叉 Meta 解析:Ethereum ProgPoW 的规范、参数与安全边界
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
本文以 EIPS/eip-1588.md 为骨架,完整继承其硬分叉规范定义,并深度融合其所引用的 EIPS/eip-1057.md 算法规范与 assets/eip-1057/test-vectors.md 测试向量,带读者系统理解 ProgPoW 的动机、参数调优、核心代码流程与安全考量。读完后,你将能准确描述该硬分叉的激活条件与包含范围,理解 ProgPoW 相比 Ethash 的六大结构性改动,掌握算法参数表、关键伪代码与验证测试向量的使用方法,并了解其已公开的安全审计结论与未修复漏洞的边界。
一、EIP-1588:一次以 ProgPoW 为核心的备选硬分叉
EIP-1588 是一份Meta 类型的 EIP,其作用并非定义新的协议机制,而是"打包"一组改动,为它们赋予统一的代号、激活时机与范围。根据 EIPS/eip-1588.md 的 Abstract,它规范了名为Ethereum ProgPoW的备选以太坊硬分叉所包含的全部变更。
该文档的 Specification 非常简洁,全部内容如下:
- 代号(Codename):Ethereum ProgPoW
- 别名(Aliases):N/A(无)
- 激活条件(Activation):以太坊主网
Block >= 7280000 - 包含的 EIP:EIP-1057——ProgPoW,一种程序化工作量证明算法(Programmatic Proof-of-Work)
这是一份典型的"薄壳"型 meta-EIP:所有实质性的技术内容都沉淀在被引用的 EIP-1057 中。EIP-1588 的意义在于为社区讨论与节点协调提供一个统一的指代——当开发者或研究者提到"Ethereum ProgPoW 分叉"时,指的是"在主网 7,280,000 区块高度激活 ProgPoW"这一整套方案。值得注意的是,该 EIP 当前状态为Stagnant(停滞),意味着它并未进入最终采纳流程,历史背景是 ProgPoW 的争议与以太坊最终转向 PoS 共识,但本文仍以仓库文档为准介绍其规范内容。
从仓库检索可以看到,ProgPoW 曾长期出现在以太坊核心开发者会议的候选清单中:EIPS/eip-2378.md 的 EIP-1057 状态表将其标记为ELIGIBLE(合格候选),并注明讨论日期为 2019-11-01,可见该提案曾是一条真实的、进入过治理流程的技术路线,而非纸面空想。
二、为什么需要 ProgPoW:ASIC 抗性之争
ProgPoW 的设计动机,源自以太坊黄皮书对 PoW 的两条根本诉求(引自 EIPS/eip-1057.md 的 Motivation 部分):
- 尽可能让更多人参与:尽量减少对专用、稀有硬件的需求或奖励,使"用电换以太"的转换率对全球任何人都大致公平;
- 杜绝超线性收益:不允许高资金壁垒的参与者攫取不成比例的算力与奖励,从而威胁网络安全。
Ethash 选择了"顺序内存硬"(sequential memory-hard)这条路线来对抗 ASIC——它要求确定 nonce 需要大量内存与带宽,使得内存无法并行服务多个 nonce 求解。但 EIP-1057 作者指出,以太坊区块链 5 年运行经验暴露了这条路线的新问题:Ethash 专用 ASIC(如 Antminer E3)已能提供高于 GPU 的效率,当时估计网络中可能多达40% 的算力由 ASIC 保障,这直接违背了黄皮书的两条设计目标。
ProgPoW 的应对思路是走黄皮书所说的"第二条路径"——让"专用硬件"的定义本身变成通用硬件:使算法的资源需求与市售 GPU 的特性完全匹配,从而让为算法定制的 ASIC 几乎没有效率优化空间。EIP-1057 给出的量化结论是:即便实现 ProgPoW 的定制 ASIC,其相对 GPU 的效率增益也仅有约1.1~1.2 倍,远低于 Ethash 的约 2 倍、CryptoNight 的约 50 倍、以及 SHA256 / Scrypt / X11 等算法的约 1000 倍。
2.1 ProgPoW 的六大核心元素
EIP-1057 将算法改动归纳为以下六点:
| 元素 | 说明 |
|---|---|
| 替换 Keccak 变体 | 从 keccak_f1600(64 位字)改为 keccak_f800(32 位字),降低功耗占比 |
| 增大 mix 状态 | 增加算法中间混合状态的规模 |
| 随机数学序列 | 主循环中加入随机序列的数学运算 |
| 小容量低延迟缓存读取 | 新增对支持随机寻址的小型低延迟缓存的读取 |
| 扩大 DRAM 读取 | 单次 DAG 读取从 128 字节提升到 256 字节 |
| 周期性程序更换 | 随机序列每隔PROGPOW_PERIOD块更换一次(约 2~12 分钟,取决于配置) |
其中"周期性程序更换"是 ProgPoW 的灵魂:随机序列变化时,挖矿软件在宿主机 CPU 上为这段序列生成源码并现场编译,GPU 执行的是"数学操作与 mix 状态均已解析完毕"的编译产物。由于程序每PROGPOW_PERIOD(默认 10 块,约 2 分钟)就更换一次,矿工没有足够时间手工优化特定随机序列——这正是"Programmatic(程序化)"一词的由来。
2.2 既有 PoW 算法的 ASIC 效率增益对比
EIP-1057 对主流 PoW 算法做了系统性比较,下表完整继承了原文档的估算数据("潜在 ASIC 效率增益"):
| 算法 | 潜在 ASIC 效率增益 | 原因 |
|---|---|---|
| SHA256 | ~1000X | 纯简单算术/逻辑/旋转指令序列,ASIC 上一颗哈希核只需少量晶体管与连线 |
| Scrypt / NeoScrypt | ~1000X | 运算与 SHA 类似,且 scratchpad 仅 32~128KB,可轻易放进 ASIC 芯片内 |
| X11 / X16R | ~1000X | 多个哈希核按固定流水线或简单状态机排序,单核效率与 SHA 相当 |
| Equihash | ~100X | ~150MB 状态虽大但 ASIC 可实现,分箱/排序/比较可超高速完成 |
| Cuckoo Cycle | ~100X | 专用图遍历核的增益与 SHA 计算核相当 |
| CryptoNight | ~50X | 需完整 2MB scratchpad,片上 SRAM 主导实现,限制哈希核数量 |
| Ethash | ~2X | DAG 体积大需外部内存,但只需最小计算,ASIC 可退化为"内存接口 + 小计算引擎" |
| ProgPoW | ~1.1~1.2X | GPU 绝大部分资源被算法用满,仅可移除图形管线与浮点单元等 |
EIP-1057 还给出了一个犀利的概念辨析:CPU 和 GPU 本身就是"通用 ASIC",任何能在其上运行的算法理论上都能被定制 ASIC 以略少的功能实现。因此"ASIC 抗性"的准确定义应是——专用硬件与广泛采用硬件之间的效率差距;差距越小,抗性越强,算法越好。这一度量标准是本文后续理解所有设计取舍的钥匙。
三、算法规范:参数、原语与核心流程
ProgPoW 的全部行为由一组可调参数决定(下表完整继承 EIPS/eip-1057.md 的参数定义):
| 参数 | 含义 |
|---|---|
PROGPOW_PERIOD | 更换随机程序的块数间隔 |
PROGPOW_LANES | 协同计算单个哈希实例的并行 lane 数 |
PROGPOW_REGS | 寄存器文件使用规模 |
PROGPOW_DAG_LOADS | 每个 lane 从 DAG 加载的 uint32 数量 |
PROGPOW_CACHE_BYTES | 缓存大小(字节) |
PROGPOW_CNT_DAG | DAG 访问次数,即算法外层循环(64 与 Ethash 相同) |
PROGPOW_CNT_CACHE | 每轮循环的缓存访问次数 |
PROGPOW_CNT_MATH | 每轮循环的数学运算次数 |
EIP-1057 文档给出了 0.9.2 与 0.9.3 两版参数演进对照表,此处完整保留:
| 参数 | 0.9.2 | 0.9.3 |
|---|---|---|
PROGPOW_PERIOD | 50 | 10 |
PROGPOW_LANES | 16 | 16 |
PROGPOW_REGS | 32 | 32 |
PROGPOW_DAG_LOADS | 4 | 4 |
PROGPOW_CACHE_BYTES | 16x1024 | 16x1024 |
PROGPOW_CNT_DAG | 64 | 64 |
PROGPOW_CNT_CACHE | 12 | 11 |
PROGPOW_CNT_MATH | 20 | 18 |
以及 DAG 参数:
| DAG 参数 | 0.9.2 | 0.9.3 |
|---|---|---|
ETHASH_DATASET_PARENTS | 256 | 256 |
关键解读:
PROGPOW_PERIOD从 50 降到 10:程序更换更频繁(约 2 分钟一次),进一步压缩手工优化随机序列的时间窗。原文档特别说明,若程序仅随 DAG epoch 更换(约 5 天一次),部分矿工将有时间针对随机序列开发手工优化版本,从而获得不当优势。- 数值约定:规范代码为 C++,所有数值均按无符号 32 位整数计算,溢出在下一次计算前截断。Python、JavaScript 等非定长数值语言,以及 Java 等仅用有符号整数的语言,实现时需特别注意位宽语义。这一设计与现代 GPU 的内部数据架构(32 位)保持一致。
3.1 两个基础随机/合并原语
FNV1a(32 位变体)——用于合并数据。Ethash 使用 FNV1,而 ProgPoW 改用分布性质更优的 FNV1a:
const uint32_t FNV_PRIME = 0x1000193; const uint32_t FNV_OFFSET_BASIS = 0x811c9dc5; uint32_t fnv1a(uint32_t h, uint32_t d) { return (h ^ d) * FNV_PRIME; }KISS99——用于随机数生成。它是指令数最少、且能通过 TestU01 统计测试套件的随机生成器;之所以不用 Mersenne Twister 这类更复杂的生成器,是因为它能在专用 ASIC 上被高效实现,反而给 ASIC 留下效率优化的空间:
typedef struct { uint32_t z, w, jsr, jcong; } kiss99_t; uint32_t kiss99(kiss99_t &st) { st.z = 36969 * (st.z & 65535) + (st.z >> 16); st.w = 18000 * (st.w & 65535) + (st.w >> 16); uint32_t MWC = ((st.z << 16) + st.w); st.jsr ^= (st.jsr << 17); st.jsr ^= (st.jsr >> 13); st.jsr ^= (st.jsr << 5); st.jcong = 69069 * st.jcong + 1234567; return ((MWC^st.jcong) + st.jsr); }3.2 mix 状态初始化:fill_mix
fill_mix用 FNV 将"每 warp 的种子"扩展为"每 lane 的种子",再用 KISS99 填充每个 lane 的mix数组(规模为PROGPOW_REGS):
void fill_mix( uint64_t seed, uint32_t lane_id, uint32_t mix[PROGPOW_REGS] ) { uint32_t fnv_hash = FNV_OFFSET_BASIS; kiss99_t st; st.z = fnv1a(fnv_hash, seed); st.w = fnv1a(fnv_hash, seed >> 32); st.jsr = fnv1a(fnv_hash, lane_id); st.jcong = fnv1a(fnv_hash, lane_id); for (int i = 0; i < PROGPOW_REGS; i++) mix[i] = kiss99(st); }3.3 哈希原语:keccak_f800
与 Ethash 一样,Keccak 用于按 nonce 播种序列并产出最终结果。ProgPoW 采用keccak-f800变体,因为其 32 位字宽与现代 GPU 原生字宽匹配。其实现是 SHAKE 的变体:width=800、bitrate=576、capacity=224、output=256、无填充。Keccak 结果按 256 位大端数处理(结果第 0 字节为 MSB)。
由于输入输出固定且较小,只需单次 absorb 与 squeeze:
hash32_t keccak_f800_progpow(uint32_t* state) { // 单次 absorb 的 keccak_f800 调用 for (int r = 0; r < 22; r++) keccak_f800_round(st, r); // 固定 8 字输出的 squeeze 阶段 hash32_t ret; for (int i=0; i<8; i++) ret.uint32s[i] = st[i]; return ret; }3.4 程序种子初始化:progPowInit
progPowInit从prog_seed生成两个序列:mix_seq_dst(merge 的目标寄存器序列)与mix_seq_src(缓存读取的源寄存器序列)。它保证每个目标恰好被 merge 一次、缓存读取无重复(重复读取可能被优化掉),并使用 Fisher-Yates 洗牌实现随机化:
kiss99_t progPowInit(uint64_t prog_seed, int mix_seq_dst[PROGPOW_REGS], int mix_seq_src[PROGPOW_REGS]) { kiss99_t prog_rnd; prog_rnd.z = fnv1a(FNV_OFFSET_BASIS, prog_seed); prog_rnd.w = fnv1a(prog_rnd.z, prog_seed >> 32); prog_rnd.jsr = fnv1a(prog_rnd.w, prog_seed); prog_rnd.jcong = fnv1a(prog_rnd.jsr, prog_seed >> 32); // 生成 merge() 的 mix 目标序列与缓存读取的 mix 源序列 for (int i = 0; i < PROGPOW_REGS; i++) { mix_seq_dst[i] = i; mix_seq_src[i] = i; } for (int i = PROGPOW_REGS - 1; i > 0; i--) { int j; j = kiss99(prog_rnd) % (i + 1); swap(mix_seq_dst[i], mix_seq_dst[j]); j = kiss99(prog_rnd) % (i + 1); swap(mix_seq_src[i], mix_seq_src[j]); } return prog_rnd; }3.5 数据合并与随机数学:merge 与 math
merge将新数据 b 并入高熵值 a,刻意只保留熵的操作(不做a & b这类会降低熵的运算):
uint32_t merge(uint32_t a, uint32_t b, uint32_t r) { switch (r % 4) { case 0: return (a * 33) + b; case 1: return (a ^ b) * 33; // 避免旋转 0 位(那是 NOP) case 2: return ROTL32(a, ((r >> 16) % 31) + 1) ^ b; case 3: return ROTR32(a, ((r >> 16) % 31) + 1) ^ b; } }math的 11 种运算全部选取自 CUDA/OpenCL 的原生指令——mul_hi、min、clz、popcount、rotate等均与 OpenCL 内建函数一一对应,ROTR32 等价于rotate(i, 32-v)。这正是"算法要求匹配 GPU 硬件"原则在指令层面的落地:
uint32_t math(uint32_t a, uint32_t b, uint32_t r) { switch (r % 11) { case 0: return a + b; case 1: return a * b; case 2: return mul_hi(a, b); case 3: return min(a, b); case 4: return ROTL32(a, b); case 5: return ROTR32(a, b); case 6: return a & b; case 7: return a | b; case 8: return a ^ b; case 9: return clz(a) + clz(b); case 10: return popcount(a) + popcount(b); } }3.6 内层循环:progPowLoop
progPowLoop的流程(每轮迭代):
- 选择
loop % LANES号 lane 作为本轮的leader; - 用 leader 的
mix[0]值对 DAG 中 256 字节条目数取模,决定从完整 DAG 何处读取; - 每个 lane 以
(lane ^ loop) % LANES作为条目内起始偏移,顺序读取DAG_LOADS个字; - 执行随机序列的数学运算与缓存访问;
- 在循环末尾合并本轮读到的 DAG 数据(延迟到最后合并,是为了充分隐藏内存延迟)。
dag参数指向以 32 位无符号整数(小端)分组的 Ethash DAG 字节;在小端架构上,它就是一个指向现有 DAG 的普通 int32 指针。DAG_BYTES为当前 DAG 的字节数,其生成方式与既有 Ethash 完全一致。原文档还解释了(l ^ loop)混洗的深层意图:防止多芯片 ASIC 各自只存储 DAG 的一部分。
void progPowLoop( const uint64_t prog_seed, const uint32_t loop, uint32_t mix[PROGPOW_LANES][PROGPOW_REGS], const uint32_t *dag) { // dag_entry 保存从 DAG 加载的 256 字节数据 uint32_t dag_entry[PROGPOW_LANES][PROGPOW_DAG_LOADS]; // 每轮旋转哪个 lane 作为 DAG 地址来源,保证上一轮 DAG 数据反馈到本轮地址 uint32_t dag_addr_base = mix[loop%PROGPOW_LANES][0] % (DAG_BYTES / (PROGPOW_LANES*PROGPOW_DAG_LOADS*sizeof(uint32_t))); for (int l = 0; l < PROGPOW_LANES; l++) { size_t dag_addr_lane = dag_addr_base * PROGPOW_LANES + (l ^ loop) % PROGPOW_LANES; for (int i = 0; i < PROGPOW_DAG_LOADS; i++) dag_entry[l][i] = dag[dag_addr_lane * PROGPOW_DAG_LOADS + i]; } int mix_seq_dst[PROGPOW_REGS]; int mix_seq_src[PROGPOW_REGS]; int mix_seq_dst_cnt = 0; int mix_seq_src_cnt = 0; kiss99_t prog_rnd = progPowInit(prog_seed, mix_seq_dst, mix_seq_src); int max_i = max(PROGPOW_CNT_CACHE, PROGPOW_CNT_MATH); for (int i = 0; i < max_i; i++) { if (i < PROGPOW_CNT_CACHE) { // 缓存内存访问:lane 访问 DAG 前部区域的随机 32 位位置 int src = mix_seq_src[(mix_seq_src_cnt++)%PROGPOW_REGS]; int dst = mix_seq_dst[(mix_seq_dst_cnt++)%PROGPOW_REGS]; int sel = kiss99(prog_rnd); for (int l = 0; l < PROGPOW_LANES; l++) { uint32_t offset = mix[l][src] % (PROGPOW_CACHE_BYTES/sizeof(uint32_t)); mix[l][dst] = merge(mix[l][dst], dag[offset], sel); } } if (i < PROGPOW_CNT_MATH) { // 随机数学:生成两个唯一源寄存器 int src_rnd = kiss99(prog_rnd) % (PROGPOW_REGS * (PROGPOW_REGS-1)); int src1 = src_rnd % PROGPOW_REGS; int src2 = src_rnd / PROGPOW_REGS; if (src2 >= src1) ++src2; // src2 现在是不等于 src1 的任意寄存器 int sel1 = kiss99(prog_rnd); int dst = mix_seq_dst[(mix_seq_dst_cnt++)%PROGPOW_REGS]; int sel2 = kiss99(prog_rnd); for (int l = 0; l < PROGPOW_LANES; l++) { uint32_t data = math(mix[l][src1], mix[l][src2], sel1); mix[l][dst] = merge(mix[l][dst], data, sel2); } } } // 在循环最末尾消费全局加载数据,以充分隐藏延迟 // 始终合并进 mix[0] 以喂养下一次偏移计算 for (int i = 0; i < PROGPOW_DAG_LOADS; i++) { int dst = (i==0) ? 0 : mix_seq_dst[(mix_seq_dst_cnt++)%PROGPOW_REGS]; int sel = kiss99(prog_rnd); for (int l = 0; l < PROGPOW_LANES; l++) mix[l][dst] = merge(mix[l][dst], dag_entry[l][i], sel); } }3.7 整体流程:progPowHash
算法整体分为六个阶段,与 Ethash 保持"header+nonce 播种 → 循环混合 → 归约 → 最终哈希"的宏观结构:
- 对 header + nonce 做一次 keccak_f800 哈希,得到 256 位摘要(填充与 Ethash 的自定义填充一致);
- 用摘要前两个字作为种子,生成初始 mix 数据;
- 多次循环,将随机加载与随机数学反复混入 mix;
- 将所有 mix 数据归约为单个 256 位值;
- 用初始摘要 + 最终 mix 256 位值做第二次 keccak 哈希(填充一致);
- 挖矿时将最终哈希与
hash32_t目标值比较。
hash32_t progPowHash( const uint64_t prog_seed, // 值为 (block_number/PROGPOW_PERIOD) const uint64_t nonce, const hash32_t header, const uint32_t *dag // 位于显存中的 GB 级 DAG——前部区域被缓存 ) { hash32_t hash_init; hash32_t hash_final; uint32_t mix[PROGPOW_LANES][PROGPOW_REGS]; // ======== 初始 keccak 通道的 absorb 阶段 ======== { uint32_t state[25] = {0x0}; // 1st: 填充 header 数据(8 字) for (int i = 0; i < 8; i++) state[i] = header.uint32s[i]; // 2nd: 填充 nonce(2 字) state[8] = nonce; state[9] = nonce >> 32; // 3rd: 应用填充 state[10] = 0x00000001; state[18] = 0x80008081; hash_init = keccak_f800_progpow(state); // 取种子以初始化 mix seed = ((uint64_t)hash_init.uint32s[1] << 32) | hash_init.uint32s[0]); } // 为所有 lane 初始化 mix for (int l = 0; l < PROGPOW_LANES; l++) fill_mix(seed, l, mix[l]); // 执行随机生成的内层循环 for (int i = 0; i < PROGPOW_CNT_DAG; i++) progPowLoop(prog_seed, i, mix, dag); // 将 mix 数据归约为每 lane 的 32 位摘要 uint32_t digest_lane[PROGPOW_LANES]; for (int l = 0; l < PROGPOW_LANES; l++) { digest_lane[l] = FNV_OFFSET_BASIS; for (int i = 0; i < PROGPOW_REGS; i++) digest_lane[l] = fnv1a(digest_lane[l], mix[l][i]); } // 将所有 lane 归约为单个 256 位摘要 for (int i = 0; i < 8; i++) digest.uint32s[i] = FNV_OFFSET_BASIS; for (int l = 0; l < PROGPOW_LANES; l++) digest.uint32s[l%8] = fnv1a(digest.uint32s[l%8], digest_lane[l]); // ======== 最终 keccak 通道的 absorb 阶段 ======== { uint32_t state[25] = {0x0}; // 1st: 填充 hash_init(8 字) for (int i = 0; i < 8; i++) state[i] = hash_init.uint32s[i]; // 2nd: 填充主循环摘要(8 字) for (int i = 8; i < 16; i++) state[i] = digest.uint32s[i - 8]; // 3rd: 应用填充 state[17] = 0x00000001; state[24] = 0x80008081; hash_final = keccak_f800_progpow(state); } // 将最终哈希与目标值比较 [...] }四、测试向量:验证实现正确性的唯一标准
ProgPoW 的工程可验证性极强——仓库在 assets/eip-1057/ 目录下提供了三份测试资产:人类可读的 assets/eip-1057/test-vectors.md、机器可读的 assets/eip-1057/test-vectors-0.9.2.json 与 assets/eip-1057/test-vectors-0.9.3.json。任何语言/平台的实现,都可以用这些向量逐函数校准。
4.1 基础原语向量
- fnv1a:如
fnv1a(0X811C9DC5, 0XDDD0A47B) = 0XD37EE61A; - kiss99:以
z=362436069, w=521288629, jsr=123456789, jcong=380116160为初态,第 1 次调用得769445856,第 100,000 次调用得941074834; - fill_mix:以
hash_seed=0xEE304846DDD0A47B、lane_id=0填充的 mix 数组,首值为0x10C02F0D, 0x99891C9E, ...,lane_id=13 时则有另一组完全不同的 32 个值; - keccak_f800_progpow:两个官方用例,给定 header 8 字与 seed 后产出固定 digest;
- merge / math:每个分支路径都有对应向量,例如
merge的mul/add、xor/mul、rotl/xor、rotr/xor四路径,math的 add、mul、mul_hi32、min、rotl32、rotr32、and、or、xor、clz、popcount 共 11 种运算。
4.2 progPowInit 与 progPowLoop 向量
对于 ProgPow period 600(即块 30,000),progPowInit应产出长度为 32 的 src 序列(0x1A, 0x1E, 0x01, ...)、dst 序列(0x00, 0x04, 0x1B, ...)以及 KISS99 状态(z=0x6535921C, w=0x29345B16, jsr=0xC0DD7F78, jcong=0x1165D7EB)。
progPowLoop向量则给出块 30,000、第 1 轮循环后mix[0]到mix[15]全部 16 条 lane 的完整 mix 数组快照——任何一行数值不匹配都意味着实现存在偏差,这是逐 lane、逐寄存器级别的严格校验。
4.3 progPowHash 端到端向量
以块 30,000 为例,EIP-1057 文档给出了完整链路:
Header : 0xffeeddccbbaa9988776655443322110000112233445566778899aabbccddeeff Nonce : 0x123456789abcdef0 Hash init : 0xee304846ddd0a47b98179e96b60ec5ceeae2727834367e593de780e3e6d1892f Mix seed : 0x7ba4d0dd464830ee Mix hash : 0x493c13e9807440571511b561132834bbd558dddaa3b70c09515080a6a1aff6d0 Hash final : 0x46b72b75f238bea3fcfd227e0027dc173dceaa1fb71744bd3d5e030ed2fed053test-vectors.md 的progPowHash一节还补充了 0.9.2 与 0.9.3 两个版本的序列化向量(Block 0、49、50、99、29,950、29,999、30,000、30,049、30,050、30,099、59,950、59,999 直至 Block 100,000,000),覆盖prog_seed从 0 到 2,000,000 的跨度,并标注了prog_seed = block_number / PROGPOW_PERIOD的计算关系。其中 0.9.2 与 0.9.3 的差异正对应上文参数表中PROGPOW_CNT_CACHE(12→11)与PROGPOW_CNT_MATH(20→18)的调整,是理解参数如何影响输出结果的第一手资料。
五、安全考量:审计结论、已知漏洞与修复边界
EIP-1057 是极少数同时接受过软件与硬件双重审计的 PoW 提案,原文档记录了以下关键结论:
1. 软件审计(Least Authority):审计方建议修改 DAG 生成——将ETHASH_DATASET_PARENTS从 256 提升到 512,以缓解"Light Evaluation"(轻量求值)攻击。副作用是 ProgPoW 的 DAG 内存文件将不再与 Ethash 兼容(epoch 长度与体积增长比例不变)。但 EIP 作者明确建议暂不实施此修复:"Ethash 数年之内不会可被利用,也不清楚 ProgPoW 是否会被利用,部署经过审计的代码是更优选择。"
2. 硬件审计(Bob Rao):作为配套的 ASIC 实现可行性评估,结论同样支撑"效率增益有限"的核心论点。
3. Kik 发现的绕过内存硬性漏洞:该漏洞同样存在于 Ethash,但近不可利用;而在 ProgPoW 中实际不可利用——它假设攻击者能像比特币那样构造候选块 header 的变体,而以太坊做不到这一点。攻击者需要修改块 header、需要接受修改后 header 的定制节点、并以非常规方式使用 extraNonce/extraData 作为熵,且暴力搜索难以在一个出块时间内完成;即便定制节点支持,其产出的块也会因 header 哈希无效而被其他对等节点立即拒绝。
4. 类似漏洞与建议修复:作者后来发现了与 Kik 类似的另一漏洞,但其开销过大,反而对 ASIC 不友好。若要彻底防范此类利用,可将最后一次 keccak 通道的输入状态从
- header(256 位)+ mix 种子(64 位)+ 主循环 mix(256 位)+ 无填充
改为
- 初始 keccak 摘要(256 位)+ 主循环 mix(256 位)+ 填充
从而把 keccak 暴力搜索的约束宽度从 64 位扩大到 256 位。该修复以 PR 形式存在于参考实现中,但作者同样建议暂不实施。
5. 影响边界:原文档特别强调,上述漏洞均无法用于拒绝服务、双花或破坏网络,最坏情况只是给部署者带来对普通矿工的效率优势。
六、实现参考与许可证说明
EIP-1057 的参考挖矿实现位于 @ifdefelse 的 ProgPOW 仓库(文档链接指向 GitHub 外部仓库,本文仅转述其事实):它是 ethminer 的衍生品,因此保留GPL 许可证——这对想基于参考实现二次开发的团队是一个重要的合规前提。
七、总结
EIP-1588 以一份极简的 meta-EIP 将 ProgPoW(EIP-1057)打包为"Ethereum ProgPoW"备选硬分叉,激活区块门槛为主网 7,280,000。剥开这层薄壳,其技术内核是一套为商品化 GPU"量身定制"的工作量证明:用 keccak_f800、FNV1a、KISS99 与 Fisher-Yates 洗牌构建随机程序,用PROGPOW_PERIOD(10 块)级的高频程序更换阻止手工优化,用覆盖全部 11 种 GPU 原生指令的math运算填满通用硬件资源,最终把定制 ASIC 的相对效率增益压到约 1.1~1.2 倍。该提案曾进入核心开发者会议候选流程(见 EIPS/eip-2378.md),最终状态为 Stagnant,但其完整的参数表、伪代码与测试向量体系,至今仍是研究"程序化抗 ASIC PoW"设计的范本。若要继续深挖,可直接研读 EIPS/eip-1057.md 全文、assets/eip-1057/test-vectors.md 的逐函数向量,以及两份 JSON 机器可读向量文件。
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考