EIP-: ```md
Authors |
---|
---
eip: 1057
title: ProgPoW,一种程序化的工作量证明
author: Greg Colvin <greg@colvin.org>, Andrea Lanfranchi (@AndreaLanfranchi), Michael Carter (@bitsbetrippin), IfDefElse <ifdefelse@protonmail.com>
discussions-to: https://ethereum-magicians.org/t/eip-progpow-a-programmatic-proof-of-work/272
status: Stagnant
type: Standards Track
category: Core
created: 2018-05-02
---
## 简单总结
一种新的工作量证明算法,用于取代 Ethash,它利用了商品 GPU 的几乎所有部分。
## 摘要
ProgPoW 是一种工作量证明算法,旨在缩小专用 ASIC 可利用的效率差距。它利用了商品硬件(GPU)的几乎所有部分,并且经过预先调整,适用于以太坊网络中最常用的硬件。
本文档概述了该算法,并研究了“抗 ASIC”的含义。接下来,我们将通过分析每种算法如何在硬件中执行来比较现有的 PoW 设计。最后,我们将通过代码详细介绍实现过程。
## 动机
自从第一个比特币挖矿 ASIC 发布以来,人们创造了许多新的工作量证明算法,其目的是“抗 ASIC”。“抗 ASIC”的目标是抵抗 PoW 挖矿能力的中心化,从而使这些币不会轻易地被少数玩家操纵。
以太坊的方法是激励一个地域分布的矿工社区,在商品硬件上降低准入门槛。正如[黄皮书](https://ethereum.github.io/yellowpaper/paper.pdf)中所述:
> 11.5. 挖矿工作量证明。挖矿工作量证明 (PoW) 作为一个密码学上安全的随机数存在,它无可辩驳地证明了在确定某个令牌值 n 时已经消耗了特定的计算量。它被用来通过赋予难度(以及扩展的,总难度)概念以意义和可信度来加强区块链的安全性。然而,由于挖掘新区块会带来附加奖励,因此工作量证明不仅充当了一种确保区块链在未来保持规范性的信心的方法,而且还充当了一种财富分配机制。
> 出于这两个原因,工作量证明函数有两个重要的目标:首先,它应该尽可能地被尽可能多的人访问。应该尽量减少对专用和不常见硬件的要求或奖励。这使得分配模型尽可能开放,并且理想情况下,使得挖矿行为成为世界上任何人在大致相同的速率下从电力到以太币的简单交换。
> 其次,不可能获得超线性利润,尤其是在具有高初始障碍的情况下。这种机制允许资金充足的攻击者获得网络总算力的令人不安的量,因此给予他们超线性奖励(从而偏向于他们),同时也降低了网络安全性……
> ... 虽然存在用于工作量证明函数的 ASIC,但这两种目标都处于危险之中。因此,已被确定为众所周知的灵丹妙药的是一种抗 ASIC 的工作量证明函数(即,难以或经济上难以在专用计算硬件中实现)。
正是基于这些前提,Ethash 被设计为一种抗 ASIC 的工作量证明:
> 存在两种 ASIC 抗性方法;首先,使其成为顺序内存硬,即设计该函数,使得随机数的确定需要大量的内存和带宽,从而无法并行使用内存来同时发现多个随机数。第二种是使其需要执行的计算类型具有通用性;对于通用任务集来说,“专用硬件”的含义自然是通用硬件,因此商品台式计算机很可能非常接近该任务的“专用硬件”。对于以太坊 1.0,我们选择了第一条道路。
以太坊区块链 5 年的经验证明了我们方法的成功。这种成功不能被认为是理所当然的。
* PoW 区块链 11 年的经验表明,硬件开发集中化,导致[少数公司](https://www.asicminervalue.com/) 控制着新硬件的生命周期,且分销有限。
* 新的 Ethash ASIC 提供了比 GPU 更高的效率,例如 [Antminer E3](https://shop.bitmain.com/product/detail?pid=00020181031134626816gh0zYNKC06A3)。
* 以太坊网络中多达 40% 的算力可能现在由 ASIC 保护。
ProgPow 通过扩展 Ethash 并采用 GPU 特定的方法来实现第二条路径,即让 PoW 任务的“专用硬件”成为商品硬件,从而恢复 Ethash 的 ASIC 抗性。
### ProgPoW 概述
ProgPoW 的设计目标是使算法的要求与商品 GPU 上可用的资源相匹配:如果该算法要在定制 ASIC 上实现,与商品 GPU 相比,效率提升的机会应该很小。
该算法的主要元素是:
* 将 keccak_f1600(具有 64 位字)更改为 keccak_f800(具有 32 位字)以减少对总功率的影响
* 增加混合状态。
* 在主循环中添加一个随机数学序列。
* 添加从支持随机地址的小型低延迟缓存的读取。
* 将 DRAM 读取从 128 字节增加到 256 字节。
随机序列每 `PROGPOW_PERIOD` 改变一次(大约 2 到 12 分钟,取决于配置的值)。当挖掘源代码时,会为随机序列生成源代码并在主机 CPU 上编译。GPU 将执行编译后的代码,其中要执行的数学运算和要使用的混合状态已经确定。
虽然仍然可以定制 ASIC 来实现此算法,但可实现的效率提升极小。商品 GPU 的大部分需要支持上述元素。唯一可用的优化是:
* 移除图形管线(显示器、几何引擎、纹理等)
* 移除浮点数学
* 一些 ISA 调整,例如与 merge() 函数完全匹配的指令
这些将导致最小的效率提升,大约为 1.1-1.2 倍。这远小于 Ethash 的 2 倍或 Cryptonight 的 50 倍。
### 在商品硬件上使用 PoW 的理由
随着大型矿池的增长,哈希算力的控制权已委托给前几个矿池,以为小型矿工提供更稳定的经济回报。虽然有些人认为大型中心化矿池破坏了“抗 ASIC”的目的,但重要的是要注意,基于 ASIC 的币的中心化程度更高,原因有以下几个。
1. 没有自然分布:超专业硬件除了挖矿之外没有经济用途,因此大多数人没有理由拥有它。
2. 没有储备组:因此,当币价波动且具有操纵吸引力时,没有硬件储备池或感兴趣方的储备池可以介入。
3. 高准入门槛:最初的矿工是那些有足够的资金投资于新币可能进行的未知实验的资本和生态资源的人。因此,通过挖矿进行的初始币分配将非常有限,从而导致中心化的经济偏差。
4. 委托中心化与实现中心化:虽然矿池中心化是委托的,但硬件单一文化并非如此:只有这种硬件的有限买家才能参与,因此甚至不可能在短期内剥离控制权。
5. 即使在去中心化挖矿的情况下,也无法明显地去中心化控制权:一旦大型定制 ASIC 制造商进入游戏,设计后门硬件是微不足道的。ASIC 制造商没有动力在市场参与中保持透明或公平。
虽然“抗 ASIC”的目标很有价值,但“抗 ASIC”的整个概念有点谬论。CPU 和 GPU 本身就是 ASIC。任何可以在商品 ASIC(CPU 或 GPU)上运行的算法,根据定义都可以创建具有略少功能的定制 ASIC。某些算法是故意设计成“ASIC 友好”的 - 其中 ASIC 实现比在通用硬件上运行的相同算法效率要高得多。当该币未知时,它提供的保护也使其成为专用挖矿 ASIC 公司的有吸引力的目标,只要它变得有用。
因此,ASIC 抗性是:专用硬件与具有更广泛采用和适用性的硬件之间的效率差异。定制硬件与通用硬件之间较小的效率差异意味着更高的抗性和更好的算法。这种效率差异是比较 PoW 算法质量时应使用的适当指标。效率可能意味着绝对性能、每瓦性能或每美元性能 - 它们都高度相关。如果单个实体创建并控制效率高得多的 ASIC,则他们可以获得网络哈希率的 51% 并可能发动攻击。
### 现有 PoW 算法的回顾
#### SHA256
* 潜在的 ASIC 效率增益 ~ 1000 倍
SHA 算法是一系列简单的数学运算 - 加法、逻辑运算和旋转。
要在 CPU 或 GPU 上处理单个操作,需要获取和解码指令,从寄存器文件中读取数据,执行指令,然后将结果写回寄存器文件。这需要大量的时间和电力。
在 ASIC 中实现的单个操作需要少量的晶体管和导线。这意味着每个单独的操作消耗的电力、面积或时间可以忽略不计。哈希核心是通过布局所需的操作序列来构建的。
与在 CPU 或 GPU 上执行相同的序列相比,哈希核心可以在更短的时间内执行所需的操作序列,并且使用更少的电力或面积。比特币 ASIC 由多个相同的哈希核心和一些最小的芯片外通信组成。
#### Scrypt 和 NeoScrypt
* 潜在的 ASIC 效率增益 ~ 1000 倍
Scrypt 和 NeoScrypt 在使用的算术和按位运算方面与 SHA 相似。不幸的是,诸如莱特币之类的流行币仅使用 32kb 到 128kb 之间的暂存空间作为其 PoW 挖矿算法。这个暂存空间很小,可以轻松地安装在 ASIC 上,放在数学核心旁边。数学核心的实现与 SHA 非常相似,具有相似的效率增益。
#### X11 和 X16R
* 潜在的 ASIC 效率增益 ~ 1000 倍
X11(以及类似的 X##)需要一个 ASIC,该 ASIC 具有 11 个以固定序列流水线化的唯一哈希核心。每个单独的哈希核心的效率与单独的 SHA 核心相似,因此总体设计将具有相同的效率增益。
X16R 要求多个哈希核心通过一个简单的排序状态机进行交互。每个单独的核心将具有相似的效率增益,并且排序逻辑将消耗最少的电力、面积或时间。
Baikal BK-X 是一个现有的 ASIC,具有多个哈希核心和一个可编程的排序器。它已经过升级,可以启用以不同顺序排序哈希的新算法。
#### Equihash
* 潜在的 ASIC 效率增益 ~ 100 倍
大约 150mb 的状态很大,但在 ASIC 上是可能的。位串的装箱、排序和比较可以在 ASIC 上以极高的速度实现。
#### Cuckoo Cycle
* 潜在的 ASIC 效率增益 ~ 100 倍
片上所需的状态量尚不清楚,因为存在时间/内存权衡攻击。与 SHA 计算核心相比,专用图遍历核心将具有相似的效率增益。
#### CryptoNight
* 潜在的 ASIC 效率增益 ~ 50 倍
与 Scrypt 相比,CryptoNight 的计算量要少得多,并且需要整整 2mb 的暂存空间(没有已知的时间/内存权衡攻击)。大型暂存空间将主导 ASIC 实现并限制哈希核心的数量,从而限制 ASIC 的绝对性能。ASIC 将几乎完全由片上 SRAM 组成。
#### Ethash
* 潜在的 ASIC 效率增益 ~ 2 倍
由于 DAG 的巨大尺寸,Ethash 需要外部存储器。然而,这只是它所需要的 - 从存储器加载结果后进行的计算量很少。因此,定制 ASIC 可以消除 GPU 的大部分复杂性和功耗,而仅仅是一个连接到小型计算引擎的存储器接口。
## 规范
可以使用以下参数调整 ProgPoW。建议的设置已针对一系列现有的商品 GPU 进行了调整:
* `PROGPOW_PERIOD`: 更改随机程序之前的区块数
* `PROGPOW_LANES`: 协调以计算单个哈希实例的并行通道数
* `PROGPOW_REGS`: 寄存器文件使用大小
* `PROGPOW_DAG_LOADS`: 每个通道从 DAG 加载的 uint32 数
* `PROGPOW_CACHE_BYTES`: 缓存的大小
* `PROGPOW_CNT_DAG`: DAG 访问次数,定义为算法的外循环(64 与 Ethash 相同)
* `PROGPOW_CNT_CACHE`: 每次循环的缓存访问次数
* `PROGPOW_CNT_MATH`: 每次循环的数学运算次数
在 Ethereum 采用的原始版本和此处提出的版本之间,这些参数的值已进行了调整。有关详细信息,请参见[这篇 medium 文章](https://medium.com/@ifdefelse/progpow-progress-da5bb31a651b)。
| 参数 | 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` | `64` |
| `PROGPOW_CNT_CACHE` | `12` | `11` | `11` |
| `PROGPOW_CNT_MATH` | `20` | `18` | `18` |
| DAG 参数 | 0.9.2 | 0.9.3 |
|--------------------------|-------|-------|
| `ETHASH_DATASET_PARENTS` | `256` | `256` |
随机程序每隔 `PROGPOW_PERIOD` 个块(默认 `10`,大约 2 分钟)更改一次,以确保执行该算法的硬件是完全可编程的。如果程序仅每隔一个 DAG 纪元(大约 5 天)更改一次,则某些矿工可能有时间开发手动优化的随机序列版本,从而使他们获得不应有的优势。
示例代码以 C++ 编写,在评估规范中的代码时应牢记这一点。所有数值都使用无符号 32 位整数计算。在进行下一次计算之前,任何溢出都会被截断。使用未固定为位长度的数值(例如 Python 和 JavaScript)或仅使用有符号整数(例如 Java)的语言需要牢记其语言的怪癖。广泛使用 32 位数据值与现代 GPU 的内部数据架构对齐。
ProgPoW 使用 **FNV1a** 的 32 位变体来合并数据。现有的 Ethash 使用了 FNV1 的类似变体进行合并,但 FNV1a 提供了更好的分配属性。
测试向量可以在[测试向量文件中](/docs/eips/assets/eip-1057/test-vectors.html#fnv1a)找到。
```cpp
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;
}
ProgPow 使用 KISS99 进行随机数生成。这是通过 TestU01 统计测试套件的最简单(指令最少)的随机生成器。像 Mersenne Twister 这样的更复杂的随机数生成器可以在专用 ASIC 上有效地实现,从而提供效率增益的机会。
测试向量可以在测试向量文件中找到。
typedef struct {
uint32_t z, w, jsr, jcong;
} kiss99_t;
// KISS99 简单、快速,并且通过了 TestU01 套件
// https://en.wikipedia.org/wiki/KISS_(algorithm)
// http://www.cse.yorku.ca/~oz/marsaglia-rng.html
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);
}
fill_mix
函数填充一个 int32
值数组,该数组由哈希计算中的每个通道使用。
测试向量可以在 测试向量文件 中找到。
void fill_mix(
uint64_t seed,
uint32_t lane_id,
uint32_t mix[PROGPOW_REGS]
)
{
// 使用 FNV 将每个 warp 的种子扩展到每个 lane
// 使用 KISS 将每个 lane 的种子扩展到填充 mix
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);
}
与 Ethash 一样,Keccak 用于为每个 nonce 播种序列并产生最终结果。keccak-f800 变体被使用,因为 32 位字大小与现代 GPU 的本机字大小匹配。该实现是 SHAKE 的变体,宽度为 800,比特率为 576,容量为 224,输出为 256,并且没有填充。keccak 的结果被视为 256 位大端数,即结果字节 0 是该值的 MSB。
与 Ethash 一样,keccak 函数的输入和输出是固定的并且相对较小。这意味着只需要一个“吸收”和“挤压”阶段。有关 keccak_f800_round
函数的伪代码实现,请参见官方 Keccak 规范的“排列的伪代码描述”部分中的 Round[b](A,RC)
函数。
hash32_t keccak_f800_progpow(uint32_t* state)
{
// keccak_f800 调用用于单次吸收
for (int r = 0; r < 22; r++)
keccak_f800_round(st, r);
// 挤压阶段,用于固定 8 个字的输出
hash32_t ret;
for (int i=0; i<8; i++)
ret.uint32s[i] = st[i];
return ret;
}
内部循环使用 FNV and KISS99 从 prog_seed
生成一个随机序列。这个随机序列确定访问哪个混合状态以及执行什么随机数学运算。
由于 prog_seed
仅在每个 PROGPOW_PERIOD
更改一次(10 个区块或大约 2 分钟),因此预期在挖矿时将在 CPU 上评估 progPowLoop
以生成该周期序列的源代码。源代码将在 CPU 上编译,然后再在 GPU 上运行。你可以在 kernel.cu 中看到示例序列和生成的源代码。
测试向量可以在测试向量文件中找到。
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() 的混合目标和用于缓存读取的混合源的随机序列
// 保证每个目标都合并一次
// 保证没有重复的缓存读取,可以优化掉
// 使用 Fisher-Yates 洗牌
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;
}
将值合并到混合数据中的数学运算是为保持熵而选择的。
测试向量可以在测试向量文件中找到。
// 将来自 b 的新数据合并到 a 中的值中
// 假设 A 具有高熵,则仅执行保留熵的运算
// 即使 B 是低熵
// (例如,不要执行 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;
}
}
为随机数学运算选择的数学运算很容易在 CUDA 和 OpenCL 中实现,这两种是商品 GPU 的主要编程语言。mul_hi、min、clz 和 popcount 函数与相应的 OpenCL 函数匹配。ROTL32 与 OpenCL rotate 函数匹配。ROTR32 是向右旋转,它等效于 rotate(i, 32-v)
。
测试向量可以在测试向量文件中找到。
// 两个输入值之间的随机数学运算
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);
}
}
内部循环的流程是:
- 通道
(loop % LANES)
被选为该循环迭代的领导者 - 领导者的
mix[0]
值模 256 字节 DAG 条目的数量,用于选择从完整 DAG 中读取的位置 - 每个通道读取
DAG_LOADS
顺序字,使用(lane ^ loop) % LANES
作为条目内的起始偏移量。 - 执行随机数学和缓存访问序列
- 在循环开始时读取的 DAG 数据在循环结束时合并
prog_seed
和 loop
来自外部循环,对应于当前程序种子(即 block_number/PROGPOW_PERIOD)和循环迭代次数。mix
是状态数组,最初由 fill_mix
填充。dag
是 Ethash DAG 的字节,以小端格式分组为 32 位无符号整数。在小端架构上,这只是指向现有 DAG 的普通 int32 指针。
DAG_BYTES
设置为当前 DAG 中的字节数,其生成方式与现有 Ethash 算法相同。
测试向量可以在测试向量文件中找到。
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];
// 在每次循环迭代时,旋转哪个通道是 DAG 地址的来源。
// 来源通道的 mix[0] 值用于确保最后一个循环的 DAG 数据馈送到此循环的地址中。
// dag_addr_base 是将要访问的 DAG 内的哪个 256 字节条目
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++)
{
// 通道从 dag 条目访问 DAG_LOADS 顺序字
// 通过 XORing 通道和循环,每个迭代都会混洗各通道访问的条目部分。
// 这可以防止多芯片 ASIC 仅存储 DAG 的一部分
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];
}
// 初始化程序种子和序列
// 在挖掘时,这些会在 CPU 上评估并编译掉
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)
{
// 缓存的内存访问
// 通道访问 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)
{
// 随机数学运算
// 生成 2 个唯一的来源
int src_rnd = kiss99(prog_rnd) % (PROGPOW_REGS * (PROGPOW_REGS-1));
int src1 = src_rnd % PROGPOW_REGS; // 0 <= src1 < PROGPOW_REGS
int src2 = src_rnd / PROGPOW_REGS; // 0 <= src2 < PROGPOW_REGS - 1
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);
}
}
整个算法的流程是:
- 对头部 + nonce 进行 keccak 哈希,以从 keccak_f800 创建 256 位的摘要(填充与 ethash 中的自定义填充一致)
- 使用摘要的前两个字作为种子来生成初始混合数据
- 循环多次,每次将随机加载和随机数学运算哈希到混合数据中
- 将所有混合数据哈希成单个 256 位值
- 使用来自初始数据 + mix_data 最终 256 位值的进位摘要进行最终的 keccak 哈希(填充与 ethash 中的自定义填充一致)
- 挖掘时,将此最终值与
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 // 位于帧缓冲中的千兆字节 DAG - 第一部分被缓存
)
{
hash32_t hash_init;
hash32_t hash_final;
uint32_t mix[PROGPOW_LANES][PROGPOW_REGS];
/*
========================================
初始 keccak 传递的吸收阶段
========================================
*/
{
uint32_t state[25] = {0x0};
// 1st 使用头部数据填充(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;
// 启动 keccak(header..nonce)
hash_init = keccak_f800_progpow(state);
// 获取用于初始化 mix 的种子
seed = ((uint64_t)hash_init.uint32s[1] << 32) | hash_init.uint32s[0]);
}
// 为所有通道初始化 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);
// 将混合数据减少为每个通道的 32 位摘要
uint32_t digest_lane[PROGPOW_LANES];
for (int l = 0; l < PRO我们不建议目前实施此修复。Ethash 在未来数年内不会被利用,并且 ProgPoW 是否会被利用尚不清楚。最好部署经过审计的代码。
在完成审计后,[Kik](https://github.com/kik/) 的一项巧妙发现披露了一个 [绕过 ProgPoW 内存硬度的漏洞](https://github.com/kik/progpow-exploit)。该漏洞也存在于 Ethash 中,但几乎不可能被利用。在 progPoW 中,利用是不可能的——它假设能够以类似于比特币的方式创建候选区块头哈希的变体,而这在以太坊中实际上是不可能的。攻击者需要修改过的区块头,需要能够接受修改过的区块头的定制节点,并将 extraNonce/extraData 用作熵——这不是标准做法。而且,所需的暴力搜索很难在一个区块时间内完成。即使定制节点支持,此类挖出的区块的传播也会立即被其他节点阻止,因为头部哈希无效。
作者们后来发现了另一个类似于 Kik 的漏洞,但它增加了太多的开销,不适合 ASIC。请参阅 Lanfranchi 的完整解释[此处](https://github.com/ifdefelse/ProgPOW/issues/51#issuecomment-690155355)。为了完全防止此类漏洞,我们可以更改修改最后一个 keccak 过程的输入状态的条件,从
* header (256 bits) +
* mix initiator 的 seed (64 bits) +
* 主循环中的 mix (256 bits)
* 无填充
改为
* 初始 keccak 的摘要 (256 bits) +
* 主循环中的 mix (256 bits) +
* 填充
从而将 keccak 中 [暴力破解 keccak 线性搜索](https://github.com/kik/progpow-exploit) 的目标约束从 64 位扩大到 256 位。
此修复程序作为 PR 提供给参考实现。同样,我们不建议目前实施此修复。Kik 的漏洞和类似漏洞现在无法利用,而且可能永远不会被利用。最好部署经过审计的代码。
请注意,这些漏洞不能被利用来拒绝服务、双重支付或以其他方式损害网络。它们最多只能让其部署者比其他矿工获得效率优势。
## 测试用例
块 30,000(prog_seed 3,000)生成的随机序列可以在 [kernel.cu](https://github.com/ifdefelse/ProgPOW/blob/824cd791634204c4cc7e31f84bb76c0c84895bd3/test/kernel.cu) 中看到。
在块 30,000 上运行的算法产生以下摘要和结果:
Header : 0xffeeddccbbaa9988776655443322110000112233445566778899aabbccddeeff Nonce : 0x123456789abcdef0 Hash init : 0xee304846ddd0a47b98179e96b60ec5ceeae2727834367e593de780e3e6d1892f Mix seed : 0x7ba4d0dd464830ee Mix hash : 0x493c13e9807440571511b561132834bbd558dddaa3b70c09515080a6a1aff6d0 Hash final : 0x46b72b75f238bea3fcfd227e0027dc173dceaa1fb71744bd3d5e030ed2fed053 ```
其他测试向量可以在 测试向量文件中找到。
机器可读的测试向量(待定)
实现
ProgPoW 挖矿的参考实现位于 @ifdefelse ProgPOW 仓库。
版权
版权和相关权利通过 CC0 放弃。
位于 ProgPOW 的参考 ProgPoW 挖矿实现是 ethminer 的衍生产品,因此保留 GPL 许可。
Citation
Please cite this document as:
, "EIP-: ```md," Ethereum Improvement Proposals, no. , . [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-.